/*! * jquery.fancytree.js * Tree view control with support for lazy loading and much more. * https://github.com/mar10/fancytree/ * * Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de) * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.38.0 * @date 2021-02-09T20:03:49Z */ /** Core Fancytree module. */ // UMD wrapper for the Fancytree core module (function(factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define(["jquery", "./jquery.fancytree.ui-deps"], factory); } else if (typeof module === "object" && module.exports) { // Node/CommonJS require("./jquery.fancytree.ui-deps"); module.exports = factory(require("jquery")); } else { // Browser globals factory(jQuery); } })(function($) { "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, // Escape those characters REX_TOOLTIP = /[<>"'/]/g, // Don't escape `&` in tooltips RECURSIVE_REQUEST_ERROR = "$recursive_request", INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid", ENTITY_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }, 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: "=", // 91: null, 93: null, // ignore left and right meta 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: "'", }, MODIFIERS = { 16: "shift", 17: "ctrl", 18: "alt", 91: "meta", 93: "meta", }, 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 attributes, that can be set by dict TREE_ATTRS = "columns types".split(" "), // TREE_ATTR_MAP = {}, // Top-level FancytreeNode attributes, that can be set by dict NODE_ATTRS = "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type 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; } } // for(i=0; i t; } } return true; } /** * Deep-merge a list of objects (but replace array-type options). * * jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays. * This variant is used to merge extension defaults with user options, and should * merge objects, but override arrays (for example the `triggerStart: [...]` option * of ext-edit). Also `null` values are copied over and not skipped. * * See issue #876 * * Example: * _simpleDeepMerge({}, o1, o2); */ function _simpleDeepMerge() { var options, name, src, copy, clone, target = arguments[0] || {}, i = 1, length = arguments.length; // Handle case when target is a string or something (possible in deep copy) if (typeof target !== "object" && !$.isFunction(target)) { target = {}; } if (i === length) { throw Error("need at least two args"); } for (; i < length; i++) { // Only deal with non-null/undefined values if ((options = arguments[i]) != null) { // Extend the base object for (name in options) { if (options.hasOwnProperty(name)) { src = target[name]; copy = options[name]; // Prevent never-ending loop if (target === copy) { continue; } // Recurse if we're merging plain objects // (NOTE: unlike $.extend, we don't merge arrays, but replace them) if (copy && $.isPlainObject(copy)) { clone = src && $.isPlainObject(src) ? src : {}; // Never move original objects, clone them target[name] = _simpleDeepMerge(clone, copy); // Don't bring in undefined values } else if (copy !== undefined) { target[name] = copy; } } } } } // Return the modified object return target; } /** 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(); } return $.Deferred(function() { this.resolveWith(context, argArray); }).promise(); } function _getRejectedPromise(context, argArray) { if (context === undefined) { return $.Deferred(function() { this.reject(); }).promise(); } 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; //
  • is already returned as object (http://api.jquery.com/data/#data-html5) d = $.extend(d, json); } return d; } 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.
    * 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 ``.
    * Note: use `node.add/remove/toggleClass()` to modify. * @property {boolean} folder Folder nodes have different default icons and click behavior.
    * 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 * @property {string} iconTooltip Description used as hover popup for icon. @since 2.27 * @property {string} type Node type, used with tree.types map. @since 2.27 */ function FancytreeNode(parent, obj) { var i, l, name, cl; this.parent = parent; this.tree = parent.tree; this.ul = null; this.li = null; //
  • 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] && (this.tree.options.copyFunctionsToData || !$.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])); } this.tree._callHook( "treeStructureChanged", this.tree, "setChildren" ); }, /** * 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 { // Returns null if insertBefore is not a direct child: 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"); }, /** * (experimental) Apply a modification (or navigation) operation. * * @param {string} cmd * @param {object} [opts] * @see Fancytree#applyCommand * @since 2.32 */ applyCommand: function(cmd, opts) { return this.tree.applyCommand(cmd, this, opts); }, /** * 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) { if (patch.hasOwnProperty(name)) { 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, FancytreeNode) 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 >= 4 (prepending node info) * * @param {*} msg string or object or array of such */ debug: function(msg) { if (this.tree.options.debugLevel >= 4) { 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); }, /** Write error to browser console if debugLevel >= 1 (prepending tree info) * * @param {*} msg string or object or array of such */ error: function(msg) { if (this.tree.options.debugLevel >= 1) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("error", arguments); } }, /**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; }, /** Find a node relative to self. * * @param {number|string} where The keyCode that would normally trigger this move, * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up'). * @returns {FancytreeNode} * @since v2.31 */ findRelatedNode: function(where, includeHidden) { return this.tree.findRelatedNode(this, where, includeHidden); }, /* 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); if (node.radiogroup) { // #931: don't (de)select this branch return "skip"; } }); 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; } } } // eslint-disable-next-line no-nested-ternary 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; } // #939: Keep a `partsel` flag that was explicitly set on a lazy node if ( node.partsel && !node.selected && node.lazy && node.children == null ) { state = undefined; } 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; } } } // eslint-disable-next-line no-nested-ternary 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". * * (Unlike `node.getPath()`, this method prepends a "/" and inverts the first argument.) * * @see FancytreeNode#getPath * @param {boolean} [excludeSelf=false] * @returns {string} */ getKeyPath: function(excludeSelf) { var sep = this.tree.options.keyPathSeparator; return sep + this.getPath(!excludeSelf, "key", 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 a string representing the hierachical node path, e.g. "a/b/c". * @param {boolean} [includeSelf=true] * @param {string | function} [part="title"] node property name or callback * @param {string} [separator="/"] * @returns {string} * @since v2.31 */ getPath: function(includeSelf, part, separator) { includeSelf = includeSelf !== false; part = part || "title"; separator = separator || "/"; var val, path = [], isFunc = $.isFunction(part); this.visitParents(function(n) { if (n.parent) { val = isFunc ? part(n) : n[part]; path.unshift(val); } }, includeSelf); return path.join(separator); }, /** 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 `className` defined in .extraClasses. * * @param {string} className class name (separate multiple classes by space) * @returns {boolean} * * @since 2.32 */ hasClass: function(className) { return ( (" " + (this.extraClasses || "") + " ").indexOf( " " + className + " " ) >= 0 ); }, /** 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 >= 3 (prepending node info) * * @param {*} msg string or object or array of such */ info: function(msg) { if (this.tree.options.debugLevel >= 3) { 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 vertically below `otherNode`, i.e. rendered in a subsequent row. * @param {FancytreeNode} otherNode * @returns {boolean} * @since 2.28 */ isBelowOf: function(otherNode) { return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5); }, /** 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; } if (p === p.parent) { $.error("Recursive parent link: " + p); } 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, n, hasFilter = this.tree.enableFilter, parents = this.getParentList(false, false); // TODO: check $(n.span).is(":visible") // i.e. return false for nodes (but not parents) that are hidden // by a filter if (hasFilter && !this.match && !this.subMatchCount) { // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); return false; } for (i = 0, l = parents.length; i < l; i++) { n = parents[i]; if (!n.expanded) { // this.debug("isVisible: HIDDEN (parent collapsed)"); return false; } // if (hasFilter && !n.match && !n.subMatchCount) { // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); // return false; // } } // this.debug("isVisible: VISIBLE"); return true; }, /** Deprecated. * @deprecated since 2014-02-16: use load() instead. */ lazyLoad: function(discard) { $.error( "FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead." ); }, /** * Load all children of a lazy node if neccessary. The expanded 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, self = 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() { self.render(); }); } else { res.always(function() { self.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, self = 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--) { // self.debug("pushexpand" + parents[i]); deferreds.push(parents[i].setExpanded(true, opts)); } $.when.apply($, deferreds).done(function() { // All expands have finished // self.debug("expand DONE", scroll); if (scroll) { self.scrollIntoView(effects).done(function() { // self.debug("scroll DONE"); dfd.resolve(); }); } else { dfd.resolve(); } }); return dfd.promise(); }, /** Move this node to targetNode. * @param {FancytreeNode} targetNode * @param {string} mode
    		 *      '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.
    * @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, tree = this.tree, 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