UNPKG

koui

Version:

Knockout UI widgets to help Knockout app development

568 lines (567 loc) 26.7 kB
/*eslint no-console: [0], no-unused-vars: [2, { args: "none" }] */ /// <amd-dependency path="jqueryui" /> define(["require", "exports", "knockout", "jquery", "./utils", "./contextmenu", "./engine", "jqueryui"], function (require, exports, ko, $, utils, ctx, engine_1) { "use strict"; var STATE_CACHE_KEY = "__SPA_TREE_STATE__", STATE_ACTIVE_KEY = "__SPA_TREE_ACTIVE_ITEM__"; exports.defaults = { cssClass: "ui-tree", isDraggable: true, isDropTarget: true, canAddChildren: true, childType: "folder", renameAfterAdd: true, connectToSortable: false, dragCursorAt: { left: 28, bottom: 0 }, dragCursor: "auto", dragHelper: function (event, element) { return $("<div></div>").addClass("drag-icon").append($("<span></span>").addClass(this.cssClass())); } }; var Tree = (function () { function Tree(options) { this.engine = engine_1.defaultInstance; this.defaults = {}; this.handlers = { selectNode: function (node, onSuccess) { console.log("select node " + node.name()); onSuccess(); }, addNode: function (parent, type, name, onSuccess) { console.log("add new ui.treeNode"); onSuccess({ id: 10, parent: parent, name: name }); // create node data to pass back }, renameNode: function (node, from, to, onSuccess) { console.log("rename node '" + from + "' to '" + to + "'"); onSuccess(); }, deleteNode: function (node, action, onSuccess) { console.log("delete node '" + node.name() + "'"); onSuccess(); }, moveNode: function (node, newParent, newIndex, onSuccess) { console.log("move node '" + node.name() + "' to '" + newParent.name ? newParent.name() : "root" + "'"); onSuccess(); }, doubleClick: function (node) { console.log("doubled clicked " + node.name()); }, rightClick: function (node) { console.log("right click " + node.name()); }, startDrag: function (node) { console.log("start drag"); }, endDrag: function (node) { console.log("stop drag"); } }; this.isDragging = ko.observable(false); this.tree = null; ko.utils.extend(options.defaults || {}, exports.defaults); ko.utils.extend(this.defaults, options.defaults); ko.utils.extend(this.handlers, options.handlers || {}); this.id = utils.createObservable(options.id); this.remember = utils.createObservable(options.remember, false); this.dragHolder = utils.createObservable(options.dragHolder); this.children = utils.createObservableArray(options.children, this.createNode, this); this.selectedNode = utils.createObservable(options.selectedNode); if (options.contextMenu) { this.contextMenu = new ctx.ContextMenuBuilder(options.contextMenu); } } Tree.prototype.findNode = function (id) { var children = this.children(), i = 0, len = children.length, node, result; for (; i < len; i++) { node = children[i]; if (node.id() === id) { result = node; } else { result = node.findNode(id); } if (result) { break; } } return result; }; Tree.prototype.selectNode = function (id, root) { var level = root || this, children = level.children(), i = 0, len = children.length, node; for (; i < len; i++) { node = children[i]; if (node.id() === id) { node.selectNode(); break; } if (this.selectNode(id, node)) { node.isOpen(true); break; } } return i < len; // found }; Tree.prototype.addNode = function (node) { var _this = this; if (node instanceof TreeNode) { // if you add a full blown node we do not call the handler to create a new ui.treeNode simply add to the tree var selected = node.isSelected(); node.isSelected(false); node.setViewModel(this); node.parent(this); this.children.push(node); selected && node.selectNode(); } else { if ((node !== undefined && node.parent === undefined) || this.children().length === 0) { // add to root var type = node.type !== undefined ? node.type : typeValueOrDefault("childType", undefined, this), name = node.name !== undefined ? node.name : typeValueOrDefault("name", type, this), rename = node.rename !== undefined ? node.rename : typeValueOrDefault("renameAfterAdd", type, this); this.handlers.addNode(undefined, type, name, function (data) { if (data !== undefined) { var newNode = new TreeNode(data, _this, _this); _this.children.push(newNode); var selected = newNode.isSelected(); if (selected || rename) { newNode.isSelected(false); newNode.selectNode(); } rename && newNode.isRenaming(true); _this.recalculateSizes(); newNode.saveState(); } }); } else { this.selectedNode().addChild(node || {}); } } }; Tree.prototype.renameNode = function (node) { node = node || this.selectedNode(); node && node.isRenaming(true); }; Tree.prototype.deleteNode = function (action, node) { node = node || this.selectedNode(); node && node.deleteSelf(action); }; Tree.prototype.deleteAll = function () { this.children().forEach(function (node) { return node.deleteSelf(); }); }; Tree.prototype.clear = function () { this.children([]); }; Tree.prototype.recalculateSizes = function () { var maxNodeWidth = 0; $(".node:visible", this.tree).each(function (ind1, node) { var newWidth = 0, $this = $(node); newWidth = newWidth + $this.children("label").outerWidth(true); newWidth = newWidth + $this.children(".icon").outerWidth(true); newWidth = newWidth + $this.children(".handle").outerWidth(true); if (maxNodeWidth < newWidth) { maxNodeWidth = newWidth; } }); $(".node", this.tree).css("minWidth", maxNodeWidth + 5); }; Tree.prototype.createNode = function (node, index) { if (node instanceof TreeNode) { node.setViewModel(this); node.parent(this); return node; } else { return new TreeNode(node, this, this, index); } }; return Tree; }()); exports.Tree = Tree; var TreeNode = (function () { function TreeNode(options, parent, viewModel, index) { var _this = this; this.viewModel = viewModel; this.isDragging = ko.observable(false); var defaultType = typeValueOrDefault("childType", parent === viewModel ? undefined : parent.type(), viewModel); // defaults this.parent = ko.observable(parent); this.contextMenu = viewModel ? viewModel.contextMenu : null; this.id = utils.createObservable(options.id); this.name = utils.createObservable(options.name); this.type = utils.createObservable(options.type, defaultType); this.cssClass = utils.createObservable(options.cssClass, this.type()); this.iconCssClass = utils.createObservable(options.iconCssClass, ""); this.index = utils.createObservable(options.index, typeof index === "undefined" ? parent.children().length : index); this.remember = parent.remember; this.isOpen = utils.createObservable(options.isOpen, false); this.isSelected = utils.createObservable(options.isSelected, false); this.isRenaming = utils.createObservable(options.isRenaming, false); this.isDragging = ko.observable(false); this.contents = options.contents; // a placeholder for additional custom data this.children = utils.createObservableArray(options.children, this.createChild, this); this.canAddChildren = ko.pureComputed(function () { return typeValueOrDefault("canAddChildren", _this.type(), _this.viewModel); }); this.showAddBefore = ko.pureComputed(function () { var parent = _this.parent(), dragHolder = viewModel.dragHolder(); if (parent.canAddChildren && parent.canAddChildren() && parent.isDropTarget && parent.isDropTarget() && viewModel.isDragging()) { if (dragHolder === _this) { return false; } else if (dragHolder.parent() === parent) { return (dragHolder.index() - _this.index()) !== -1; } else { return true; } } return false; }); this.showAddAfter = ko.pureComputed(function () { var parent = _this.parent(), dragHolder = viewModel.dragHolder(); if (parent.canAddChildren && parent.canAddChildren() && parent.isDropTarget() && viewModel.isDragging()) { if (dragHolder === _this) { return false; } else if (dragHolder.parent() === parent) { return (dragHolder.index() - _this.index()) !== 1; } else { return true; } } return false; }); this.isDropTarget = ko.pureComputed(function () { return typeValueOrDefault("isDropTarget", _this.type(), _this.viewModel); }); this.connectToSortable = ko.pureComputed(function () { return typeValueOrDefault("connectToSortable", _this.type(), _this.viewModel); }); this.isDraggable = ko.pureComputed(function () { var childRenaming = _this.children().some(function (child) { return child.isRenaming(); }), typeDefault = typeValueOrDefault("isDraggable", _this.type(), _this.viewModel); return !_this.isRenaming() && !childRenaming && typeDefault; }); this.level = ko.pureComputed(function () { try { var plevel = _this.parent().level(); return plevel + 1; } catch (err) { return 0; } }); this.loadState(); utils.bindAll(this, "toggle", "clicked", "doubleClick"); } TreeNode.prototype.hasChildren = function () { return this.children().length > 0; }; TreeNode.prototype.hasContext = function () { return !!this.contextMenu; }; TreeNode.prototype.uniqueIdentifier = function () { return this.viewModel.id() + this.type() + this.id(); }; TreeNode.prototype.createChild = function (node, index) { if (node instanceof TreeNode) { node.setViewModel(this.viewModel); node.parent(this); return node; } else { return new TreeNode(node, this, this.viewModel, index); } }; TreeNode.prototype.setViewModel = function (viewModel) { this.children().forEach(function (child) { return child.setViewModel(viewModel); }); this.viewModel = viewModel; this.contextMenu = viewModel.contextMenu; }; TreeNode.prototype.findNode = function (id) { var children = this.children(), i = 0, len = children.length, node, result; for (; i < len; i++) { node = children[i]; if (node.id() === id) { result = node; } else { result = node.findNode(id); } if (result) { break; } } return result; }; TreeNode.prototype.selectNode = function () { var _this = this; var selected = this.viewModel.selectedNode(); if (selected !== undefined && selected.isRenaming()) { $(".rename > .node input", this.viewModel.tree).blur(); } this.saveState(); if (selected === undefined || (selected !== undefined && selected !== this)) { this.viewModel.handlers.selectNode(this, function () { if (selected !== undefined) { selected.isSelected(false); selected.isRenaming(false); } _this.openParents(); _this.isSelected(true); _this.viewModel.selectedNode(_this); _this.saveState(); }); } }; TreeNode.prototype.openParents = function () { var current = this.parent(); while (current.parent !== undefined) { current.isOpen(true); current = current.parent(); } }; TreeNode.prototype.openSelfAndParents = function () { this.isOpen(true); this.openParents(); }; TreeNode.prototype.toggle = function () { this.isOpen(!this.isOpen()); this.viewModel.recalculateSizes(); this.saveState(); }; TreeNode.prototype.addChild = function (node) { var _this = this; if (this.canAddChildren()) { var defaultType = typeValueOrDefault("childType", this.type(), this.viewModel), type = node.type !== undefined ? node.type : defaultType, defaultName = typeValueOrDefault("name", type, this.viewModel), name = node.name !== undefined ? node.name : defaultName, rename = node.rename !== undefined ? node.rename : typeValueOrDefault("renameAfterAdd", type, this.viewModel); // the addNode handler must return an id for the new ui.treeNode this.viewModel.handlers.addNode(this, type, name, function (data) { if (data !== undefined) { var newNode = new TreeNode(data, _this, _this.viewModel); _this.children.push(newNode); _this.openSelfAndParents(); var selected = newNode.isSelected(); if (selected || rename) { newNode.isSelected(false); _this.isSelected(false); newNode.selectNode(); } rename && newNode.isRenaming(true); _this.viewModel.recalculateSizes(); _this.saveState(); } }); } }; TreeNode.prototype.rename = function (newName) { var _this = this; this.viewModel.handlers.renameNode(this, this.name(), newName, function () { _this.name(newName); _this.viewModel.recalculateSizes(); }); }; TreeNode.prototype.deleteSelf = function (action) { var _this = this; this.viewModel.handlers.deleteNode(this, action, function () { var parent = _this.parent(); _this.children().forEach(function (child) { return child.deleteSelf(action + " child"); }); if (parent !== undefined) { parent.children.remove(_this); if (!action || action.indexOf("child") === -1) { var tIndex = _this.index(); parent.children().forEach(function (child) { var index = child.index(); (index > tIndex) && child.index(index - 1); }); } } _this.viewModel.recalculateSizes(); }); }; TreeNode.prototype.move = function (node, parent, index) { var _this = this; var newParent = parent || this, oldParent = node.parent(), newIndex = typeof index === "undefined" ? newParent.children().length : index, oldIndex = node.index(); this.viewModel.handlers.moveNode(node, newParent, newIndex, function () { oldParent.children.remove(node); oldParent.children().forEach(function (child) { var index = child.index(); (index > oldIndex) && child.index(index - 1); }); node.index(newIndex); node.parent(newParent); oldParent.children().forEach(function (child) { var index = child.index(); (index >= newIndex) && child.index(index + 1); }); newParent.children.splice(newIndex, 0, node); newParent.isOpen && newParent.isOpen(true); node.selectNode(); _this.viewModel.recalculateSizes(); _this.saveState(); }); }; TreeNode.prototype.moveBefore = function (node) { this.move(node, this.parent(), this.index()); }; TreeNode.prototype.moveAfter = function (node) { this.move(node, this.parent(), this.index() + 1); }; TreeNode.prototype.getDragHolder = function () { return this.viewModel.dragHolder(); }; TreeNode.prototype.setDragHolder = function (event, element) { this.viewModel.dragHolder(this); }; TreeNode.prototype.loadState = function () { if (!this.remember() || !window.localStorage) { return; } var state = JSON.parse(localStorage.getItem(STATE_CACHE_KEY)), uid = this.uniqueIdentifier(); if (state) { var itemState = state[uid]; if (itemState && itemState.open === true) { this.isOpen(true); } var active = state[STATE_ACTIVE_KEY]; if (active && active === uid) { this.isSelected(true); } } }; TreeNode.prototype.saveState = function () { if (!this.remember() || !window.localStorage) { return; } var state = JSON.parse(localStorage.getItem(STATE_CACHE_KEY)) || {}, uid = this.uniqueIdentifier(); state[uid] = { open: this.isOpen() }; if (this.isSelected()) { state[STATE_ACTIVE_KEY] = uid; } localStorage.setItem(STATE_CACHE_KEY, JSON.stringify(state)); }; TreeNode.prototype.clicked = function (node, event) { if ($(event.target).is("input")) { return true; } switch (event.which) { case 1: this.selectNode(); break; case 3: this.viewModel.handlers.rightClick(this); break; } }; TreeNode.prototype.doubleClick = function (event) { this.viewModel.handlers.doubleClick(this); }; return TreeNode; }()); exports.TreeNode = TreeNode; ko.bindingHandlers.treenodedrag = { init: function (element, valueAccessor, allBindingsAccessor, viewModel) { var $element = $(element), node = viewModel, dragOptions = { revert: "invalid", revertDuration: 250, cancel: "span.handle", cursor: typeValueOrDefault("dragCursor", node.type(), node.viewModel), cursorAt: typeValueOrDefault("dragCursorAt", node.type(), node.viewModel), appendTo: "body", connectToSortable: viewModel.connectToSortable(), helper: function (event, _element) { var helper = typeValueOrDefault("dragHelper", node.type(), node.viewModel); return helper.call(viewModel, event, _element); }, zIndex: 200000, addClasses: false, distance: 10, start: function (e, ui) { viewModel.setDragHolder(); viewModel.isDragging(true); viewModel.viewModel.handlers.startDrag(viewModel); viewModel.viewModel.isDragging(true); }, stop: function (e, ui) { viewModel.isDragging(false); viewModel.viewModel.handlers.endDrag(viewModel); viewModel.viewModel.isDragging(false); } }; $element.draggable(dragOptions); }, update: function (element, valueAccessor, allBindingsAccessor, viewModel) { var $element = $(element), active = ko.unwrap(valueAccessor()); if (!active) { $element.draggable("disable"); } else { $element.draggable("enable"); } } }; ko.bindingHandlers.treenodedrop = { init: function (element, valueAccessor, allBindingsAccessor, viewModel) { var $element = $(element), value = valueAccessor() || {}, handler = ko.unwrap(value.onDropComplete), dropOptions = { greedy: true, tolerance: "pointer", addClasses: false, drop: function (e, ui) { setTimeout(function () { handler.call(viewModel, viewModel.getDragHolder()); }, 0); } }; $element.droppable(dropOptions); }, update: function (element, valueAccessor, allBindingsAccessor, viewModel) { var $element = $(element), active = ko.unwrap(valueAccessor()).active; if (!active) { $element.droppable("disable"); } else { $element.droppable("enable"); } } }; ko.bindingHandlers.treenodeselectvisible = { update: function (element, valueAccessor) { ko.bindingHandlers.visible.update(element, valueAccessor); var isCurrentlyInvisible = element.style.display === "none"; if (!isCurrentlyInvisible) { element.select(); } } }; function nodeRenameUpdateValue(element, valueAccessor, allBindingsAccessor, viewModel) { var handler = allBindingsAccessor().onRenameComplete, elementValue = ko.selectExtensions.readValue(element); handler.call(viewModel, elementValue); viewModel.isRenaming(false); } ko.bindingHandlers.treenoderename = { init: function (element, valueAccessor, allBindingsAccessor, viewModel) { var $element = $(element), updateHandler = function () { nodeRenameUpdateValue(element, valueAccessor, allBindingsAccessor, viewModel); }; $element.on({ blur: updateHandler, keyup: function (e) { return (e.which === 13) && updateHandler(); } }); }, update: function (element, valueAccessor, allBindingsAccessor, viewModel) { ko.bindingHandlers.value.update(element, valueAccessor); } }; ko.bindingHandlers.tree = { init: function (element, valueAccessor) { var value = ko.unwrap(valueAccessor()); value.tree = element; // needed to recalculate node sizes when multiple trees console.log("Initialize tree " + value.children().length + " root nodes found"); return { controlsDescendantBindings: true }; }, update: function (element, valueAccessor) { var value = ko.unwrap(valueAccessor()); ko.renderTemplate("text!koui/tree/container.html", value, { templateEngine: engine_1.defaultInstance }, element); value.recalculateSizes(); } }; //#endregion //#region Private Methods function typeValueOrDefault(param, type, viewModel, index) { var globalDefault = viewModel.defaults[param]; if (viewModel.defaults[type] === undefined || viewModel.defaults[type][param] === undefined) { return ko.unwrap(globalDefault); } return ko.unwrap(viewModel.defaults[type][param]); } }); //#endregion