UNPKG

jquery.fancytree

Version:

jQuery tree view / tree grid plugin with support for keyboard, inline editing, filtering, checkboxes, drag'n'drop, and lazy loading

1,658 lines (1,573 loc) 293 kB
/*! * jquery.fancytree.js * Tree view control with support for lazy loading and much more. * https://github.com/mar10/fancytree/ * * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de) * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.23.0 * @date 2017-05-27T20:09:38Z */ /** Core Fancytree module. */ // Start of local namespace ;(function($, window, document, undefined) { "use strict"; // prevent duplicate loading if ( $.ui && $.ui.fancytree ) { $.ui.fancytree.warn("Fancytree: ignored duplicate include"); return; } /* ***************************************************************************** * Private functions and variables */ var i, attr, FT = null, // initialized below TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/' REX_HTML = /[&<>"'\/]/g, REX_TOOLTIP = /[<>"'\/]/g, RECURSIVE_REQUEST_ERROR = "$recursive_request", ENTITY_MAP = {"&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;", "/": "&#x2F;"}, IGNORE_KEYCODES = { 16: true, 17: true, 18: true }, SPECIAL_KEYCODES = { 8: "backspace", 9: "tab", 10: "return", 13: "return", // 16: null, 17: null, 18: null, // ignore shift, ctrl, alt 19: "pause", 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111: "/", 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'"}, MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" }, // Boolean attributes that can be set with equivalent class names in the LI tags // Note: v2.23: checkbox and hideCheckbox are *not* in this list CLASS_ATTRS = "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split(" "), CLASS_ATTR_MAP = {}, // Top-level Fancytree node attributes, that can be set by dict NODE_ATTRS = "checkbox expanded extraClasses folder icon key lazy radiogroup refKey selected statusNodeType title tooltip unselectable unselectableIgnore unselectableStatus".split(" "), NODE_ATTR_MAP = {}, // Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase) NODE_ATTR_LOWERCASE_MAP = {}, // Attribute names that should NOT be added to node.data NONE_NODE_DATA_MAP = {"active": true, "children": true, "data": true, "focus": true}; for(i=0; i<CLASS_ATTRS.length; i++){ CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; } for(i=0; i<NODE_ATTRS.length; i++) { attr = NODE_ATTRS[i]; NODE_ATTR_MAP[attr] = true; if( attr !== attr.toLowerCase() ) { NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr; } } function _assert(cond, msg){ // TODO: see qunit.js extractStacktrace() if(!cond){ msg = msg ? ": " + msg : ""; // consoleApply("assert", [!!cond, msg]); $.error("Fancytree assertion failed" + msg); } } _assert($.ui, "Fancytree requires jQuery UI (http://jqueryui.com)"); function consoleApply(method, args){ var i, s, fn = window.console ? window.console[method] : null; if(fn){ try{ fn.apply(window.console, args); } catch(e) { // IE 8? s = ""; for( i=0; i<args.length; i++ ) { s += args[i]; } fn(s); } } } /*Return true if x is a FancytreeNode.*/ function _isNode(x){ return !!(x.tree && x.statusNodeType !== undefined); } /** Return true if dotted version string is equal or higher than requested version. * * See http://jsfiddle.net/mar10/FjSAN/ */ function isVersionAtLeast(dottedVersion, major, minor, patch){ var i, v, t, verParts = $.map($.trim(dottedVersion).split("."), function(e){ return parseInt(e, 10); }), testParts = $.map(Array.prototype.slice.call(arguments, 1), function(e){ return parseInt(e, 10); }); for( i = 0; i < testParts.length; i++ ){ v = verParts[i] || 0; t = testParts[i] || 0; if( v !== t ){ return ( v > t ); } } return true; } /** Return a wrapper that calls sub.methodName() and exposes * this : tree * this._local : tree.ext.EXTNAME * this._super : base.methodName.call() * this._superApply : base.methodName.apply() */ function _makeVirtualFunction(methodName, tree, base, extension, extName){ // $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName); // if(rexTestSuper && !rexTestSuper.test(func)){ // // extension.methodName() doesn't call _super(), so no wrapper required // return func; // } // Use an immediate function as closure var proxy = (function(){ var prevFunc = tree[methodName], // org. tree method or prev. proxy baseFunc = extension[methodName], // _local = tree.ext[extName], _super = function(){ return prevFunc.apply(tree, arguments); }, _superApply = function(args){ return prevFunc.apply(tree, args); }; // Return the wrapper function return function(){ var prevLocal = tree._local, prevSuper = tree._super, prevSuperApply = tree._superApply; try{ tree._local = _local; tree._super = _super; tree._superApply = _superApply; return baseFunc.apply(tree, arguments); }finally{ tree._local = prevLocal; tree._super = prevSuper; tree._superApply = prevSuperApply; } }; })(); // end of Immediate Function return proxy; } /** * Subclass `base` by creating proxy functions */ function _subclassObject(tree, base, extension, extName){ // $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName); for(var attrName in extension){ if(typeof extension[attrName] === "function"){ if(typeof tree[attrName] === "function"){ // override existing method tree[attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName); }else if(attrName.charAt(0) === "_"){ // Create private methods in tree.ext.EXTENSION namespace tree.ext[extName][attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName); }else{ $.error("Could not override tree." + attrName + ". Use prefix '_' to create tree." + extName + "._" + attrName); } }else{ // Create member variables in tree.ext.EXTENSION namespace if(attrName !== "options"){ tree.ext[extName][attrName] = extension[attrName]; } } } } function _getResolvedPromise(context, argArray){ if(context === undefined){ return $.Deferred(function(){this.resolve();}).promise(); }else{ return $.Deferred(function(){this.resolveWith(context, argArray);}).promise(); } } function _getRejectedPromise(context, argArray){ if(context === undefined){ return $.Deferred(function(){this.reject();}).promise(); }else{ return $.Deferred(function(){this.rejectWith(context, argArray);}).promise(); } } function _makeResolveFunc(deferred, context){ return function(){ deferred.resolveWith(context); }; } function _getElementDataAsDict($el){ // Evaluate 'data-NAME' attributes with special treatment for 'data-json'. var d = $.extend({}, $el.data()), json = d.json; delete d.fancytree; // added to container by widget factory (old jQuery UI) delete d.uiFancytree; // added to container by widget factory if( json ) { delete d.json; // <li data-json='...'> is already returned as object (http://api.jquery.com/data/#data-html5) d = $.extend(d, json); } return d; } function _escapeHtml(s){ return ("" + s).replace(REX_HTML, function(s) { return ENTITY_MAP[s]; }); } function _escapeTooltip(s){ return ("" + s).replace(REX_TOOLTIP, function(s) { return ENTITY_MAP[s]; }); } // TODO: use currying function _makeNodeTitleMatcher(s){ s = s.toLowerCase(); return function(node){ return node.title.toLowerCase().indexOf(s) >= 0; }; } function _makeNodeTitleStartMatcher(s){ var reMatch = new RegExp("^" + s, "i"); return function(node){ return reMatch.test(node.title); }; } /* ***************************************************************************** * FancytreeNode */ /** * Creates a new FancytreeNode * * @class FancytreeNode * @classdesc A FancytreeNode represents the hierarchical data model and operations. * * @param {FancytreeNode} parent * @param {NodeData} obj * * @property {Fancytree} tree The tree instance * @property {FancytreeNode} parent The parent node * @property {string} key Node id (must be unique inside the tree) * @property {string} title Display name (may contain HTML) * @property {object} data Contains all extra data that was passed on node creation * @property {FancytreeNode[] | null | undefined} children Array of child nodes.<br> * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array * to define a node that has no children. * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property. * @property {string} extraClasses Additional CSS classes, added to the node's `&lt;span>`.<br> * Note: use `node.add/remove/toggleClass()` to modify. * @property {boolean} folder Folder nodes have different default icons and click behavior.<br> * Note: Also non-folders may have children. * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'. * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion. * @property {boolean} selected Use isSelected(), setSelected() to access this property. * @property {string} tooltip Alternative description used as hover popup */ function FancytreeNode(parent, obj){ var i, l, name, cl; this.parent = parent; this.tree = parent.tree; this.ul = null; this.li = null; // <li id='key' ftnode=this> tag this.statusNodeType = null; // if this is a temp. node to display the status of its parent this._isLoading = false; // if this node itself is loading this._error = null; // {message: '...'} if a load error occurred this.data = {}; // TODO: merge this code with node.toDict() // copy attributes from obj object for(i=0, l=NODE_ATTRS.length; i<l; i++){ name = NODE_ATTRS[i]; this[name] = obj[name]; } // unselectableIgnore and unselectableStatus imply unselectable if( this.unselectableIgnore != null || this.unselectableStatus != null ) { this.unselectable = true; } if( obj.hideCheckbox ) { $.error("'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'"); } // node.data += obj.data if(obj.data){ $.extend(this.data, obj.data); } // copy all other attributes to this.data.NAME for(name in obj){ if(!NODE_ATTR_MAP[name] && !$.isFunction(obj[name]) && !NONE_NODE_DATA_MAP[name]){ // node.data.NAME = obj.NAME this.data[name] = obj[name]; } } // Fix missing key if( this.key == null ){ // test for null OR undefined if( this.tree.options.defaultKey ) { this.key = this.tree.options.defaultKey(this); _assert(this.key, "defaultKey() must return a unique key"); } else { this.key = "_" + (FT._nextNodeKey++); } } else { this.key = "" + this.key; // Convert to string (#217) } // Fix tree.activeNode // TODO: not elegant: we use obj.active as marker to set tree.activeNode // when loading from a dictionary. if(obj.active){ _assert(this.tree.activeNode === null, "only one active node allowed"); this.tree.activeNode = this; } if( obj.selected ){ // #186 this.tree.lastSelectedNode = this; } // TODO: handle obj.focus = true // Create child nodes cl = obj.children; if( cl ){ if( cl.length ){ this._setChildren(cl); } else { // if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded' this.children = this.lazy ? [] : null; } } else { this.children = null; } // Add to key/ref map (except for root node) // if( parent ) { this.tree._callHook("treeRegisterNode", this.tree, true, this); // } } FancytreeNode.prototype = /** @lends FancytreeNode# */{ /* Return the direct child FancytreeNode with a given key, index. */ _findDirectChild: function(ptr){ var i, l, cl = this.children; if(cl){ if(typeof ptr === "string"){ for(i=0, l=cl.length; i<l; i++){ if(cl[i].key === ptr){ return cl[i]; } } }else if(typeof ptr === "number"){ return this.children[ptr]; }else if(ptr.parent === this){ return ptr; } } return null; }, // TODO: activate() // TODO: activateSilently() /* Internal helper called in recursive addChildren sequence.*/ _setChildren: function(children){ _assert(children && (!this.children || this.children.length === 0), "only init supported"); this.children = []; for(var i=0, l=children.length; i<l; i++){ this.children.push(new FancytreeNode(this, children[i])); } }, /** * Append (or insert) a list of child nodes. * * @param {NodeData[]} children array of child node definitions (also single child accepted) * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such). * If omitted, the new children are appended. * @returns {FancytreeNode} first child added * * @see FancytreeNode#applyPatch */ addChildren: function(children, insertBefore){ var i, l, pos, origFirstChild = this.getFirstChild(), origLastChild = this.getLastChild(), firstNode = null, nodeList = []; if($.isPlainObject(children) ){ children = [children]; } if(!this.children){ this.children = []; } for(i=0, l=children.length; i<l; i++){ nodeList.push(new FancytreeNode(this, children[i])); } firstNode = nodeList[0]; if(insertBefore == null){ this.children = this.children.concat(nodeList); }else{ insertBefore = this._findDirectChild(insertBefore); pos = $.inArray(insertBefore, this.children); _assert(pos >= 0, "insertBefore must be an existing child"); // insert nodeList after children[pos] this.children.splice.apply(this.children, [pos, 0].concat(nodeList)); } if ( origFirstChild && !insertBefore ) { // #708: Fast path -- don't render every child of root, just the new ones! // #723, #729: but only if it's appended to an existing child list for(i=0, l=nodeList.length; i<l; i++) { nodeList[i].render(); // New nodes were never rendered before } // Adjust classes where status may have changed // Has a first child if (origFirstChild !== this.getFirstChild()) { // Different first child -- recompute classes origFirstChild.renderStatus(); } if (origLastChild !== this.getLastChild()) { // Different last child -- recompute classes origLastChild.renderStatus(); } } else if( !this.parent || this.parent.ul || this.tr ){ // render if the parent was rendered (or this is a root node) this.render(); } if( this.tree.options.selectMode === 3 ){ this.fixSelection3FromEndNodes(); } this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null); return firstNode; }, /** * Add class to node's span tag and to .extraClasses. * * @param {string} className class name * * @since 2.17 */ addClass: function(className){ return this.toggleClass(className, true); }, /** * Append or prepend a node, or append a child node. * * This a convenience function that calls addChildren() * * @param {NodeData} node node definition * @param {string} [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') * @returns {FancytreeNode} new node */ addNode: function(node, mode){ if(mode === undefined || mode === "over"){ mode = "child"; } switch(mode){ case "after": return this.getParent().addChildren(node, this.getNextSibling()); case "before": return this.getParent().addChildren(node, this); case "firstChild": // Insert before the first child if any var insertBefore = (this.children ? this.children[0] : null); return this.addChildren(node, insertBefore); case "child": case "over": return this.addChildren(node); } _assert(false, "Invalid mode: " + mode); }, /**Add child status nodes that indicate 'More...', etc. * * This also maintains the node's `partload` property. * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes. * @param {string} [mode='child'] 'child'|firstChild' * @since 2.15 */ addPagingNode: function(node, mode){ var i, n; mode = mode || "child"; if( node === false ) { for(i=this.children.length-1; i >= 0; i--) { n = this.children[i]; if( n.statusNodeType === "paging" ) { this.removeChild(n); } } this.partload = false; return; } node = $.extend({ title: this.tree.options.strings.moreData, statusNodeType: "paging", icon: false }, node); this.partload = true; return this.addNode(node, mode); }, /** * Append new node after this. * * This a convenience function that calls addNode(node, 'after') * * @param {NodeData} node node definition * @returns {FancytreeNode} new node */ appendSibling: function(node){ return this.addNode(node, "after"); }, /** * Modify existing child nodes. * * @param {NodePatch} patch * @returns {$.Promise} * @see FancytreeNode#addChildren */ applyPatch: function(patch) { // patch [key, null] means 'remove' if(patch === null){ this.remove(); return _getResolvedPromise(this); } // TODO: make sure that root node is not collapsed or modified // copy (most) attributes to node.ATTR or node.data.ATTR var name, promise, v, IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global for(name in patch){ v = patch[name]; if( !IGNORE_MAP[name] && !$.isFunction(v)){ if(NODE_ATTR_MAP[name]){ this[name] = v; }else{ this.data[name] = v; } } } // Remove and/or create children if(patch.hasOwnProperty("children")){ this.removeChildren(); if(patch.children){ // only if not null and not empty list // TODO: addChildren instead? this._setChildren(patch.children); } // TODO: how can we APPEND or INSERT child nodes? } if(this.isVisible()){ this.renderTitle(); this.renderStatus(); } // Expand collapse (final step, since this may be async) if(patch.hasOwnProperty("expanded")){ promise = this.setExpanded(patch.expanded); }else{ promise = _getResolvedPromise(this); } return promise; }, /** Collapse all sibling nodes. * @returns {$.Promise} */ collapseSiblings: function() { return this.tree._callHook("nodeCollapseSiblings", this); }, /** Copy this node as sibling or child of `node`. * * @param {FancytreeNode} node source node * @param {string} [mode=child] 'before' | 'after' | 'child' * @param {Function} [map] callback function(NodeData) that could modify the new node * @returns {FancytreeNode} new */ copyTo: function(node, mode, map) { return node.addNode(this.toDict(true, map), mode); }, /** Count direct and indirect children. * * @param {boolean} [deep=true] pass 'false' to only count direct children * @returns {int} number of child nodes */ countChildren: function(deep) { var cl = this.children, i, l, n; if( !cl ){ return 0; } n = cl.length; if(deep !== false){ for(i=0, l=n; i<l; i++){ n += cl[i].countChildren(); } } return n; }, // TODO: deactivate() /** Write to browser console if debugLevel >= 2 (prepending node info) * * @param {*} msg string or object or array of such */ debug: function(msg){ if( this.tree.options.debugLevel >= 2 ) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("log", arguments); } }, /** Deprecated. * @deprecated since 2014-02-16. Use resetLazy() instead. */ discard: function(){ this.warn("FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead."); return this.resetLazy(); }, /** Remove DOM elements for all descendents. May be called on .collapse event * to keep the DOM small. * @param {boolean} [includeSelf=false] */ discardMarkup: function(includeSelf){ var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup"; this.tree._callHook(fn, this); }, /**Find all nodes that match condition (excluding self). * * @param {string | function(node)} match title string to search for, or a * callback function that returns `true` if a node is matched. * @returns {FancytreeNode[]} array of nodes (may be empty) */ findAll: function(match) { match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match); var res = []; this.visit(function(n){ if(match(n)){ res.push(n); } }); return res; }, /**Find first node that matches condition (excluding self). * * @param {string | function(node)} match title string to search for, or a * callback function that returns `true` if a node is matched. * @returns {FancytreeNode} matching node or null * @see FancytreeNode#findAll */ findFirst: function(match) { match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match); var res = null; this.visit(function(n){ if(match(n)){ res = n; return false; } }); return res; }, /* Apply selection state (internal use only) */ _changeSelectStatusAttrs: function(state) { var changed = false, opts = this.tree.options, unselectable = FT.evalOption("unselectable", this, this, opts, false), unselectableStatus = FT.evalOption("unselectableStatus", this, this, opts, undefined); if( unselectable && unselectableStatus != null ) { state = unselectableStatus; } switch(state){ case false: changed = ( this.selected || this.partsel ); this.selected = false; this.partsel = false; break; case true: changed = ( !this.selected || !this.partsel ); this.selected = true; this.partsel = true; break; case undefined: changed = ( this.selected || !this.partsel ); this.selected = false; this.partsel = true; break; default: _assert(false, "invalid state: " + state); } // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); if( changed ){ this.renderStatus(); } return changed; }, /** * Fix selection status, after this node was (de)selected in multi-hier mode. * This includes (de)selecting all children. */ fixSelection3AfterClick: function(callOpts) { var flag = this.isSelected(); // this.debug("fixSelection3AfterClick()"); this.visit(function(node){ node._changeSelectStatusAttrs(flag); }); this.fixSelection3FromEndNodes(callOpts); }, /** * Fix selection status for multi-hier mode. * Only end-nodes are considered to update the descendants branch and parents. * Should be called after this node has loaded new children or after * children have been modified using the API. */ fixSelection3FromEndNodes: function(callOpts) { var opts = this.tree.options; // this.debug("fixSelection3FromEndNodes()"); _assert(opts.selectMode === 3, "expected selectMode 3"); // Visit all end nodes and adjust their parent's `selected` and `partsel` // attributes. Return selection state true, false, or undefined. function _walk(node){ var i, l, child, s, state, allSelected, someSelected, unselIgnore, unselState, children = node.children; if( children && children.length ){ // check all children recursively allSelected = true; someSelected = false; for( i=0, l=children.length; i<l; i++ ){ child = children[i]; // the selection state of a node is not relevant; we need the end-nodes s = _walk(child); // if( !child.unselectableIgnore ) { unselIgnore = FT.evalOption("unselectableIgnore", child, child, opts, false); if( !unselIgnore ) { if( s !== false ) { someSelected = true; } if( s !== true ) { allSelected = false; } } } state = allSelected ? true : (someSelected ? undefined : false); }else{ // This is an end-node: simply report the status unselState = FT.evalOption("unselectableStatus", node, node, opts, undefined); state = ( unselState == null ) ? !!node.selected : !!unselState; } node._changeSelectStatusAttrs(state); return state; } _walk(this); // Update parent's state this.visitParents(function(node){ var i, l, child, state, unselIgnore, unselState, children = node.children, allSelected = true, someSelected = false; for( i=0, l=children.length; i<l; i++ ){ child = children[i]; unselIgnore = FT.evalOption("unselectableIgnore", child, child, opts, false); if( !unselIgnore ) { unselState = FT.evalOption("unselectableStatus", child, child, opts, undefined); state = ( unselState == null ) ? !!child.selected : !!unselState; // When fixing the parents, we trust the sibling status (i.e. // we don't recurse) if( state || child.partsel ) { someSelected = true; } if( !state ) { allSelected = false; } } } state = allSelected ? true : (someSelected ? undefined : false); node._changeSelectStatusAttrs(state); }); }, // TODO: focus() /** * Update node data. If dict contains 'children', then also replace * the hole sub tree. * @param {NodeData} dict * * @see FancytreeNode#addChildren * @see FancytreeNode#applyPatch */ fromDict: function(dict) { // copy all other attributes to this.data.xxx for(var name in dict){ if(NODE_ATTR_MAP[name]){ // node.NAME = dict.NAME this[name] = dict[name]; }else if(name === "data"){ // node.data += dict.data $.extend(this.data, dict.data); }else if(!$.isFunction(dict[name]) && !NONE_NODE_DATA_MAP[name]){ // node.data.NAME = dict.NAME this.data[name] = dict[name]; } } if(dict.children){ // recursively set children and render this.removeChildren(); this.addChildren(dict.children); } this.renderTitle(); /* var children = dict.children; if(children === undefined){ this.data = $.extend(this.data, dict); this.render(); return; } dict = $.extend({}, dict); dict.children = undefined; this.data = $.extend(this.data, dict); this.removeChildren(); this.addChild(children); */ }, /** Return the list of child nodes (undefined for unexpanded lazy nodes). * @returns {FancytreeNode[] | undefined} */ getChildren: function() { if(this.hasChildren() === undefined){ // TODO: only required for lazy nodes? return undefined; // Lazy node: unloaded, currently loading, or load error } return this.children; }, /** Return the first child node or null. * @returns {FancytreeNode | null} */ getFirstChild: function() { return this.children ? this.children[0] : null; }, /** Return the 0-based child index. * @returns {int} */ getIndex: function() { // return this.parent.children.indexOf(this); return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7 }, /** Return the hierarchical child index (1-based, e.g. '3.2.4'). * @param {string} [separator="."] * @param {int} [digits=1] * @returns {string} */ getIndexHier: function(separator, digits) { separator = separator || "."; var s, res = []; $.each(this.getParentList(false, true), function(i, o){ s = "" + (o.getIndex() + 1); if( digits ){ // prepend leading zeroes s = ("0000000" + s).substr(-digits); } res.push(s); }); return res.join(separator); }, /** Return the parent keys separated by options.keyPathSeparator, e.g. "id_1/id_17/id_32". * @param {boolean} [excludeSelf=false] * @returns {string} */ getKeyPath: function(excludeSelf) { var path = [], sep = this.tree.options.keyPathSeparator; this.visitParents(function(n){ if(n.parent){ path.unshift(n.key); } }, !excludeSelf); return sep + path.join(sep); }, /** Return the last child of this node or null. * @returns {FancytreeNode | null} */ getLastChild: function() { return this.children ? this.children[this.children.length - 1] : null; }, /** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... . * @returns {int} */ getLevel: function() { var level = 0, dtn = this.parent; while( dtn ) { level++; dtn = dtn.parent; } return level; }, /** Return the successor node (under the same parent) or null. * @returns {FancytreeNode | null} */ getNextSibling: function() { // TODO: use indexOf, if available: (not in IE6) if( this.parent ){ var i, l, ac = this.parent.children; for(i=0, l=ac.length-1; i<l; i++){ // up to length-2, so next(last) = null if( ac[i] === this ){ return ac[i+1]; } } } return null; }, /** Return the parent node (null for the system root node). * @returns {FancytreeNode | null} */ getParent: function() { // TODO: return null for top-level nodes? return this.parent; }, /** Return an array of all parent nodes (top-down). * @param {boolean} [includeRoot=false] Include the invisible system root node. * @param {boolean} [includeSelf=false] Include the node itself. * @returns {FancytreeNode[]} */ getParentList: function(includeRoot, includeSelf) { var l = [], dtn = includeSelf ? this : this.parent; while( dtn ) { if( includeRoot || dtn.parent ){ l.unshift(dtn); } dtn = dtn.parent; } return l; }, /** Return the predecessor node (under the same parent) or null. * @returns {FancytreeNode | null} */ getPrevSibling: function() { if( this.parent ){ var i, l, ac = this.parent.children; for(i=1, l=ac.length; i<l; i++){ // start with 1, so prev(first) = null if( ac[i] === this ){ return ac[i-1]; } } } return null; }, /** * Return an array of selected descendant nodes. * @param {boolean} [stopOnParents=false] only return the topmost selected * node (useful with selectMode 3) * @returns {FancytreeNode[]} */ getSelectedNodes: function(stopOnParents) { var nodeList = []; this.visit(function(node){ if( node.selected ) { nodeList.push(node); if( stopOnParents === true ){ return "skip"; // stop processing this branch } } }); return nodeList; }, /** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded). * @returns {boolean | undefined} */ hasChildren: function() { if(this.lazy){ if(this.children == null ){ // null or undefined: Not yet loaded return undefined; }else if(this.children.length === 0){ // Loaded, but response was empty return false; }else if(this.children.length === 1 && this.children[0].isStatusNode() ){ // Currently loading or load error return undefined; } return true; } return !!( this.children && this.children.length ); }, /** Return true if node has keyboard focus. * @returns {boolean} */ hasFocus: function() { return (this.tree.hasFocus() && this.tree.focusNode === this); }, /** Write to browser console if debugLevel >= 1 (prepending node info) * * @param {*} msg string or object or array of such */ info: function(msg){ if( this.tree.options.debugLevel >= 1 ) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("info", arguments); } }, /** Return true if node is active (see also FancytreeNode#isSelected). * @returns {boolean} */ isActive: function() { return (this.tree.activeNode === this); }, /** Return true if node is a direct child of otherNode. * @param {FancytreeNode} otherNode * @returns {boolean} */ isChildOf: function(otherNode) { return (this.parent && this.parent === otherNode); }, /** Return true, if node is a direct or indirect sub node of otherNode. * @param {FancytreeNode} otherNode * @returns {boolean} */ isDescendantOf: function(otherNode) { if(!otherNode || otherNode.tree !== this.tree){ return false; } var p = this.parent; while( p ) { if( p === otherNode ){ return true; } p = p.parent; } return false; }, /** Return true if node is expanded. * @returns {boolean} */ isExpanded: function() { return !!this.expanded; }, /** Return true if node is the first node of its parent's children. * @returns {boolean} */ isFirstSibling: function() { var p = this.parent; return !p || p.children[0] === this; }, /** Return true if node is a folder, i.e. has the node.folder attribute set. * @returns {boolean} */ isFolder: function() { return !!this.folder; }, /** Return true if node is the last node of its parent's children. * @returns {boolean} */ isLastSibling: function() { var p = this.parent; return !p || p.children[p.children.length-1] === this; }, /** Return true if node is lazy (even if data was already loaded) * @returns {boolean} */ isLazy: function() { return !!this.lazy; }, /** Return true if node is lazy and loaded. For non-lazy nodes always return true. * @returns {boolean} */ isLoaded: function() { return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node }, /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending. * @returns {boolean} */ isLoading: function() { return !!this._isLoading; }, /* * @deprecated since v2.4.0: Use isRootNode() instead */ isRoot: function() { return this.isRootNode(); }, /** Return true if node is partially selected (tri-state). * @returns {boolean} * @since 2.23 */ isPartsel: function() { return !this.selected && !!this.partsel; }, /** (experimental) Return true if this is partially loaded. * @returns {boolean} * @since 2.15 */ isPartload: function() { return !!this.partload; }, /** Return true if this is the (invisible) system root node. * @returns {boolean} * @since 2.4 */ isRootNode: function() { return (this.tree.rootNode === this); }, /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive). * @returns {boolean} */ isSelected: function() { return !!this.selected; }, /** Return true if this node is a temporarily generated system node like * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). * @returns {boolean} */ isStatusNode: function() { return !!this.statusNodeType; }, /** Return true if this node is a status node of type 'paging'. * @returns {boolean} * @since 2.15 */ isPagingNode: function() { return this.statusNodeType === "paging"; }, /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. * @returns {boolean} * @since 2.4 */ isTopLevel: function() { return (this.tree.rootNode === this.parent); }, /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false. * @returns {boolean} */ isUndefined: function() { return this.hasChildren() === undefined; // also checks if the only child is a status node }, /** Return true if all parent nodes are expanded. Note: this does not check * whether the node is scrolled into the visible part of the screen. * @returns {boolean} */ isVisible: function() { var i, l, parents = this.getParentList(false, false); for(i=0, l=parents.length; i<l; i++){ if( ! parents[i].expanded ){ return false; } } return true; }, /** Deprecated. * @deprecated since 2014-02-16: use load() instead. */ lazyLoad: function(discard) { this.warn("FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead."); return this.load(discard); }, /** * Load all children of a lazy node if neccessary. The <i>expanded</i> state is maintained. * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded. * @returns {$.Promise} */ load: function(forceReload) { var res, source, that = this, wasExpanded = this.isExpanded(); _assert( this.isLazy(), "load() requires a lazy node" ); // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" ); if( !forceReload && !this.isUndefined() ) { return _getResolvedPromise(this); } if( this.isLoaded() ){ this.resetLazy(); // also collapses } // This method is also called by setExpanded() and loadKeyPath(), so we // have to avoid recursion. source = this.tree._triggerNodeEvent("lazyLoad", this); if( source === false ) { // #69 return _getResolvedPromise(this); } _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result"); res = this.tree._callHook("nodeLoadChildren", this, source); if( wasExpanded ) { this.expanded = true; res.always(function(){ that.render(); }); } else { res.always(function(){ that.renderStatus(); // fix expander icon to 'loaded' }); } return res; }, /** Expand all parents and optionally scroll into visible area as neccessary. * Promise is resolved, when lazy loading and animations are done. * @param {object} [opts] passed to `setExpanded()`. * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} * @returns {$.Promise} */ makeVisible: function(opts) { var i, that = this, deferreds = [], dfd = new $.Deferred(), parents = this.getParentList(false, false), len = parents.length, effects = !(opts && opts.noAnimation === true), scroll = !(opts && opts.scrollIntoView === false); // Expand bottom-up, so only the top node is animated for(i = len - 1; i >= 0; i--){ // that.debug("pushexpand" + parents[i]); deferreds.push(parents[i].setExpanded(true, opts)); } $.when.apply($, deferreds).done(function(){ // All expands have finished // that.debug("expand DONE", scroll); if( scroll ){ that.scrollIntoView(effects).done(function(){ // that.debug("scroll DONE"); dfd.resolve(); }); } else { dfd.resolve(); } }); return dfd.promise(); }, /** Move this node to targetNode. * @param {FancytreeNode} targetNode * @param {string} mode <pre> * 'child': append this node as last child of targetNode. * This is the default. To be compatble with the D'n'd * hitMode, we also accept 'over'. * 'firstChild': add this node as first child of targetNode. * 'before': add this node as sibling before targetNode. * 'after': add this node as sibling after targetNode.</pre> * @param {function} [map] optional callback(FancytreeNode) to allow modifcations */ moveTo: function(targetNode, mode, map) { if(mode === undefined || mode === "over"){ mode = "child"; } else if ( mode === "firstChild" ) { if( targetNode.children && targetNode.children.length ) { mode = "before"; targetNode = targetNode.children[0]; } else { mode = "child"; } } var pos, prevParent = this.parent, targetParent = (mode === "child") ? targetNode : targetNode.parent; if(this === targetNode){ return; }else if( !this.parent ){ $.error("Cannot move system root"); }else if( targetParent.isDescendantOf(this) ){ $.error("Cannot move a node to its own descendant"); } if( targetParent !== prevParent ) { prevParent.triggerModifyChild("remove", this); } // Unlink this node from current parent if( this.parent.children.length === 1 ) { if( this.parent === targetParent ){ return; // #258 } this.parent.children = this.parent.lazy ? [] : null; this.parent.expanded = false; } else { pos = $.inArray(this, this.parent.children); _assert(pos >= 0, "invalid source parent"); this.parent.children.splice(pos, 1); } // Remove from source DOM parent // if(this.parent.ul){ // this.parent.ul.removeChild(this.li); // } // Insert this node to target parent's child list this.parent = targetParent; if( targetParent.hasChildren() ) { switch(mode) { case "child": // Append to existing target children targetParent.children.push(this); break; case "before": // Insert this node before target node pos = $.inArray(targetNode, targetParent.children); _assert(pos >= 0, "invalid target parent"); targetParent.children.splice(pos, 0, this); break; case "after": // Insert this node after target node pos = $.inArray(targetNode, targetParent.children); _assert(pos >= 0, "invalid target parent"); targetParent.children.splice(pos+1, 0, this); break; default: $.error("Invalid mode " + mode); } } else { targetParent.children = [ this ]; } // Parent has no <ul> tag yet: // if( !targetParent.ul ) { // // This is the parent's first child: create UL tag // // (Hidden, because it will be // targetParent.ul = document.createElement("ul"); // targetParent.ul.style.display = "none"; // targetParent.li.appendChild(targetParent.ul); // } // // Issue 319: Add to target DOM parent (only if node was already rendered(expanded)) // if(this.li){ // targetParent.ul.appendChild(this.li); // }^ // Let caller modify the nodes if( map ){ targetNode.visit(map, true); } if( targetParent === prevParent ) { targetParent.triggerModifyChild("move", this); } else { // prevParent.triggerModifyChild("remove", this); targetParent.triggerModifyChild("add", this); } // Handle cross-tree moves if( this.tree !== targetNode.tree ) { // Fix node.tree for all source nodes // _assert(false, "Cross-tree move is not yet implemented."); this.warn("Cross-tree moveTo is experimantal!"); this.visit(function(n){ // TODO: fix selection state and activation, ... n.tree = targetNode.tree; }, true); } // A collaposed node won't re-render children, so we have to remove it manually // if( !targetParent.expanded ){ // prevParent.ul.removeChild(this.li); // } // Update HTML markup if( !prevParent.isDescendantOf(targetParent)) { prevParent.render(); } if( !targetParent.isDescendantOf(prevParent) && targetParent !== prevParent) { targetParent.render(); } // TODO: fix selection state // TODO: fix active state /* var tree = this.tree; var opts = tree.options; var pers = tree.persistence; // Always expand, if it's below minExpandLevel // tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel()); if ( opts.minExpandLevel >= ftnode.getLevel() ) { // tree.logDebug ("Force expand for %o", ftnode); this.bExpanded = true; } // In multi-hier mode, update the parents selection state // DT issue #82: only if not initializing, because the children may not exist yet // if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing ) // ftnode._fixSelectionState(); // In multi-hier mode, update the parents selection state if( ftnode.bSelected && opts.selectMode==3 ) { var p = this; while( p ) { if( !p.hasSubSel ) p._setSubSel(true); p = p.parent; } } // render this node and the new child if ( tree.bEnableUpdate ) this.render(); return ftnode; */ }, /** Set focus relative to this node and optionally activate. * * @param {number} where The keyCode that would normally trigger this move, * e.g. `$.ui.keyCode.LEFT` would collapse the node if it * is expanded or move to the parent oterwise. * @param {boolean} [activate=true] * @returns {$.Promise} */ navigate: function(where, activate) { var i, parents, res, handled = true, KC = $.ui.keyCode, sib = null; // Navigate to node function _goto(n){ if( n ){ // setFocus/setActive will scroll later (if autoScroll is specified) try { n.makeVisible({scrollIntoView: false}); } catch(e) {} // #272 // Node may still be hidden by a filter if( ! $(n.span).is(":visible") ) { n.debug("Navigate: skipping hidden node"); n.navigate(where, activate); return; } return activate === false ? n.setFocus() : n.setActive(); } } switch( where ) { case KC.BACKSPACE: if( this.parent && this.parent.parent ) { res = _goto(this.parent); } break; case KC.HOME: this.tree.visit(function(n){ // goto first visible node if( $(n.span).is(":visible") ) { res = _goto(n); return false; } }); break; case KC.END: this.tree.visit(function(n){ // goto last visible node if( $(n.span).is(":visible") ) { res = n; } }); if( res ) { res = _goto(res); } break; case KC.LEFT: if( this.expanded ) { this.setExpanded(false); res = _goto(this); } else if( this.parent && this.parent.parent ) { res = _goto(this.parent); } break; case KC.RIGHT: if( !this.expanded && (this.children || this.lazy) ) { this.setExpanded(); res = _goto(this); } else if( this.children && this.children.length ) { res = _goto(this.children[0]); } break; case KC.UP: sib = this.getPrevSibling(); // #359: skip hidden sibling nodes, preventing a _goto() recursion while( sib && !$(sib.span).is(":visible") ) { sib = sib.getPrevSibling(); } while( sib && sib.expanded && sib.children && sib.children.length ) { sib = sib.children[sib.children.length - 1]; } if( !sib && this.parent && this.parent.parent ){ sib = this.parent; } res = _goto(sib); break; case KC.DOWN: if( this.expanded && this.children && this.children.length ) { sib = this.children[0]; } else { parents = this.getParentList(false, true); for(i=parents.length-1; i>=0; i--) { sib = parents[i].getNextSibling(); // #359: skip hidden sibling nodes, preventing a _goto() recursion while( sib && !$(sib.span).is(":visible") ) { sib = sib.getNextSibling(); } if( sib ){ break; } } } res = _goto(sib); break; default: handled = false; } return res || _getResolvedPromise(); }, /** * Remove this node (not allowed for system root). */ remove: function() { return this.parent.removeChild(this); }, /** * Remove childNode from list of direct children. * @param {FancytreeNode} childNode */ removeChild: function(childNode) { return this.tree._callHook("nodeRemoveChild", this, childNode); }, /** * Remove all child nodes and descendents. This converts the node into a leaf.<br> * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy() * in order to trigger lazyLoad on next expand. */ removeChildren: function() { return this.tree._callHook("nodeRemoveChildren", this); }, /** * Remove class from node's span tag and .extraClasses. * * @param {string} className class name * * @since 2.17 */ removeClass: function(className){ return this.toggleClass(className, false); }, /** * This method renders and updates all HTML markup that is required * to display this node in its current state.<br> * Note: * <ul> * <li>It should only be neccessary to call this method after the node object * was modified by direct access to its properties, because the common * API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...) * already handle this. * <li> {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} * are implied. If changes are more local, calling only renderTitle() or * renderStatus() may be sufficient and faster. * </ul> * * @param {boolean} [force=false] re-render, even if html markup was already created * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed */ render: function(force, deep) { return this.tree._callHook("nodeRender", this, force, deep); }, /** Create HTML markup for the node's outer &lt;span> (expander, checkbox, icon, and title). * Implies {@link FancytreeNode#renderStatus}. * @see Fancytree_Hooks#nodeRenderTitle */ renderTitle: function() { return this.tree._callHook("nodeRenderTitle", this); }, /** Update element's CSS classes according to node state. * @see Fancytree_Hooks#nodeRenderStatus */ renderStatus: function() { return this.tree._callHook("nodeRenderStatus", this); }, /** * (experimental) Replace this node with `source`. * (Currently only available for paging nodes.) * @param {NodeData[]} source List of child node definitions * @since 2.15 */ replaceWith: function(source) { var res, parent = this.parent, pos = $.inArray(this, parent.children), that = this; _assert( this.isPagingNode(), "replaceWith() currently requires a paging status node" ); res = this.tree._callHook("nodeLoadChildren", this, source); res.done(function(data){ // New nodes are currently children of `this`. var children = that.children; // Prepend newly loaded child nodes to `this` // Move new children after self for( i=0; i<children.length; i++ ) { children[i].parent = parent; } parent.children.splice.apply(parent.children, [pos + 1, 0].concat(children)); // Remove self that.children = null; that.remove(); // Redraw new nodes parent.render(); // TODO: set node.partload = false if this was tha last paging node? // parent.addPagingNode(false); }).fail(function(){ that.setExpanded(); }); return res; // $.error("Not implemented: replaceWith()"); }, /** * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad * event is triggered on next expand. */ resetLazy: function() { this.removeChildren(); this.expanded = false; this.lazy = true; this.children = undefined; this.renderStatus(); }, /** Schedule activity for delayed execution (cancel any pending request).