/*! * jquery.fancytree.js * Dynamic tree view control, with support for lazy loading of branches. * https://github.com/mar10/fancytree/ * * Copyright (c) 2006-2014, Martin Wendt (http://wwWendt.de) * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.1.0 * @date 2014-05-29T16:44 */ /** Core Fancytree module. */ // Start of local namespace ;(function($, window, document, undefined) { "use strict"; // prevent duplicate loading if ( $.ui.fancytree && $.ui.fancytree.version ) { $.ui.fancytree.warn("Fancytree: ignored duplicate include"); return; } /* ***************************************************************************** * Private functions and variables */ function _raiseNotImplemented(msg){ msg = msg || ""; $.error("Not implemented: " + msg); } function _assert(cond, msg){ // TODO: see qunit.js extractStacktrace() if(!cond){ msg = msg ? ": " + msg : ""; $.error("Assertion failed" + msg); } } function consoleApply(method, args){ var i, s, fn = window.console ? window.console[method] : null; if(fn){ if(fn.apply){ fn.apply(window.console, args); }else{ // IE? s = ""; for( i=0; i t ); } } return true; } /** Return a wrapper that calls sub.methodName() and exposes * this : tree * this._local : tree.ext.EXTNAME * this._super : base.methodName() */ 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); }; // Return the wrapper function return function(){ var prevLocal = tree._local, prevSuper = tree._super; try{ tree._local = _local; tree._super = _super; return baseFunc.apply(tree, arguments); }finally{ tree._local = prevLocal; tree._super = prevSuper; } }; })(); // 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 if( json ) { delete d.json; //
  • is already returned as object (http://api.jquery.com/data/#data-html5) d = $.extend(d, json); } return d; } // TODO: use currying function _makeNodeTitleMatcher(s){ s = s.toLowerCase(); return function(node){ return node.title.toLowerCase().indexOf(s) >= 0; }; } var i, FT = null, // initialized below ENTITY_MAP = {"&": "&", "<": "<", ">": ">", "\"": """, "'": "'", "/": "/"}, //boolean attributes that can be set with equivalent class names in the LI tags CLASS_ATTRS = "active expanded focus folder hideCheckbox lazy selected unselectable".split(" "), CLASS_ATTR_MAP = {}, // Top-level Fancytree node attributes, that can be set by dict NODE_ATTRS = "expanded extraClasses folder hideCheckbox key lazy refKey selected title tooltip unselectable".split(" "), NODE_ATTR_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 * 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 Addtional CSS classes, added to the node's `<span>` * @property {boolean} folder Folder nodes have different default icons and click behavior.
    * Note: Also non-folders may have children. * @property {string} statusNodeType null or type of temporarily generated system node like 'loading', or 'error'. * @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 banner */ 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 occured this.data = {}; // TODO: merge this code with node.toDict() // copy attributes from obj object for(i=0, l=NODE_ATTRS.length; i= 0, "insertBefore must be an existing child"); // insert nodeList after children[pos] this.children.splice.apply(this.children, [pos, 0].concat(nodeList)); } 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(); } return firstNode; }, /** * 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', 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 "child": case "over": return this.addChildren(node); } _assert(false, "Invalid mode: " + 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 '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= 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("debug", 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(); }, // TODO: expand(flag) /**Find all nodes that contain `match` in the title. * * @param {string | function(node)} match string to search for, of a function that * returns `true` if a node is matched. * @returns {FancytreeNode[]} array of nodes (may be empty) * @see FancytreeNode#findAll */ 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 contains `match` in the title (not including self). * * @param {string | function(node)} match string to search for, of a function that * returns `true` if a node is matched. * @returns {FancytreeNode} matching node or null * @example * fat text */ 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; 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() { var flag = this.isSelected(); // this.debug("fixSelection3AfterClick()"); this.visit(function(node){ node._changeSelectStatusAttrs(flag); }); this.fixSelection3FromEndNodes(); }, /** * 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() { // this.debug("fixSelection3FromEndNodes()"); _assert(this.tree.options.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, children = node.children; if( children ){ // check all children recursively allSelected = true; someSelected = false; for( i=0, l=children.length; 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
    	 *      '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'.
    	 *      '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"; } var pos, prevParent = this.parent, targetParent = (mode === "child") ? targetNode : targetNode.parent; if(this === targetNode){ return; }else if( !this.parent ){ throw "Cannot move system root"; }else if( targetParent.isDescendantOf(this) ){ throw "Cannot move a node to its own descendant"; } // Unlink this node from current parent if( this.parent.children.length === 1 ) { this.parent.children = this.parent.lazy ? [] : null; this.parent.expanded = false; } else { pos = $.inArray(this, this.parent.children); _assert(pos >= 0); 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); targetParent.children.splice(pos, 0, this); break; case "after": // Insert this node after target node pos = $.inArray(targetNode, targetParent.children); _assert(pos >= 0); targetParent.children.splice(pos+1, 0, this); break; default: throw "Invalid mode " + mode; } } else { targetParent.children = [ this ]; } // Parent has no
      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); } // 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, handled = true, KC = $.ui.keyCode, sib = null; // Navigate to node function _goto(n){ if( n ){ n.makeVisible(); // 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 ) { _goto(this.parent); } break; case KC.LEFT: if( this.expanded ) { this.setExpanded(false); _goto(this); } else if( this.parent && this.parent.parent ) { _goto(this.parent); } break; case KC.RIGHT: if( !this.expanded && (this.children || this.lazy) ) { this.setExpanded(); _goto(this); } else if( this.children && this.children.length ) { _goto(this.children[0]); } break; case KC.UP: sib = this.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; } _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(); if( sib ){ break; } } } _goto(sib); break; default: handled = false; } }, /** * 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.
      * 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); }, /** * This method renders and updates all HTML markup that is required * to display this node in its current state.
      * Note: *
        *
      • 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. *
      • {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} * are implied. If changes are more local, calling only renderTitle() or * renderStatus() may be sufficient and faster. *
      • If a node was created/removed, node.render() must be called on the parent. *
      * * @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 (expander, checkbox, icon, and title). * @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); }, /** * 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). * scheduleAction('cancel') will only cancel a pending request (if any). * @param {string} mode * @param {number} ms */ scheduleAction: function(mode, ms) { if( this.tree.timer ) { clearTimeout(this.tree.timer); // this.tree.debug("clearTimeout(%o)", this.tree.timer); } this.tree.timer = null; var self = this; // required for closures switch (mode) { case "cancel": // Simply made sure that timer was cleared break; case "expand": this.tree.timer = setTimeout(function(){ self.tree.debug("setTimeout: trigger expand"); self.setExpanded(true); }, ms); break; case "activate": this.tree.timer = setTimeout(function(){ self.tree.debug("setTimeout: trigger activate"); self.setActive(true); }, ms); break; default: throw "Invalid mode " + mode; } // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer); }, /** * * @param {boolean | PlainObject} [effects=false] animation options. * @param {object} [options=null] {topNode: null, effects: ..., parent: ...} this node will remain visible in * any case, even if `this` is outside the scroll pane. * @returns {$.Promise} */ scrollIntoView: function(effects, options) { if( options !== undefined && _isNode(options) ) { this.warn("scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead."); options = {topNode: options}; } // this.$scrollParent = (this.options.scrollParent === "auto") ? $ul.scrollParent() : $(this.options.scrollParent); // this.$scrollParent = this.$scrollParent.length ? this.$scrollParent || this.$container; var topNodeY, nodeY, horzScrollbarHeight, containerOffsetTop, opts = $.extend({ effects: (effects === true) ? {duration: 200, queue: false} : effects, scrollOfs: this.tree.options.scrollOfs, scrollParent: this.tree.options.scrollParent || this.tree.$container, topNode: null }, options), dfd = new $.Deferred(), that = this, nodeHeight = $(this.span).height(), $container = $(opts.scrollParent), topOfs = opts.scrollOfs.top || 0, bottomOfs = opts.scrollOfs.bottom || 0, containerHeight = $container.height(),// - topOfs - bottomOfs, scrollTop = $container.scrollTop(), $animateTarget = $container, isParentWindow = $container[0] === window, topNode = opts.topNode || null, newScrollTop = null; // this.debug("scrollIntoView(), scrollTop=", scrollTop, opts.scrollOfs); _assert($(this.span).is(":visible"), "scrollIntoView node is invisible"); // otherwise we cannot calc offsets if( isParentWindow ) { nodeY = $(this.span).offset().top; topNodeY = topNode ? $(topNode.span).offset().top : 0; $animateTarget = $("html,body"); } else { _assert($container[0] !== document && $container[0] !== document.body, "scrollParent should be an simple element or `window`, not document or body."); containerOffsetTop = $container.offset().top, nodeY = $(this.span).offset().top - containerOffsetTop + scrollTop; // relative to scroll parent topNodeY = topNode ? $(topNode.span).offset().top - containerOffsetTop + scrollTop : 0; horzScrollbarHeight = Math.max(0, ($container.innerHeight() - $container[0].clientHeight)); containerHeight -= horzScrollbarHeight; } // this.debug(" scrollIntoView(), nodeY=", nodeY, "containerHeight=", containerHeight); if( nodeY < (scrollTop + topOfs) ){ // Node is above visible container area newScrollTop = nodeY - topOfs; // this.debug(" scrollIntoView(), UPPER newScrollTop=", newScrollTop); }else if((nodeY + nodeHeight) > (scrollTop + containerHeight - bottomOfs)){ newScrollTop = nodeY + nodeHeight - containerHeight + bottomOfs; // this.debug(" scrollIntoView(), LOWER newScrollTop=", newScrollTop); // If a topNode was passed, make sure that it is never scrolled // outside the upper border if(topNode){ _assert($(topNode.span).is(":visible")); if( topNodeY < newScrollTop ){ newScrollTop = topNodeY - topOfs; // this.debug(" scrollIntoView(), TOP newScrollTop=", newScrollTop); } } } if(newScrollTop !== null){ // this.debug(" scrollIntoView(), SET newScrollTop=", newScrollTop); if(opts.effects){ opts.effects.complete = function(){ dfd.resolveWith(that); }; $animateTarget.stop(true).animate({ scrollTop: newScrollTop }, opts.effects); }else{ $animateTarget[0].scrollTop = newScrollTop; dfd.resolveWith(this); } }else{ dfd.resolveWith(this); } return dfd.promise(); }, /**Activate this node. * @param {boolean} [flag=true] pass false to deactivate * @param {object} [opts] additional options. Defaults to {noEvents: false} */ setActive: function(flag, opts){ return this.tree._callHook("nodeSetActive", this, flag, opts); }, /**Expand or collapse this node. Promise is resolved, when lazy loading and animations are done. * @param {boolean} [flag=true] pass false to collapse * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false} * @returns {$.Promise} */ setExpanded: function(flag, opts){ return this.tree._callHook("nodeSetExpanded", this, flag, opts); }, /**Set keyboard focus to this node. * @param {boolean} [flag=true] pass false to blur * @see Fancytree#setFocus */ setFocus: function(flag){ return this.tree._callHook("nodeSetFocus", this, flag); }, // TODO: setLazyNodeStatus /**Select this node, i.e. check the checkbox. * @param {boolean} [flag=true] pass false to deselect */ setSelected: function(flag){ return this.tree._callHook("nodeSetSelected", this, flag); }, /**Rename this node. * @param {string} title */ setTitle: function(title){ this.title = title; this.renderTitle(); }, /**Sort child list by title. * @param {function} [cmp] custom compare function(a, b) that returns -1, 0, or 1 (defaults to sort by title). * @param {boolean} [deep=false] pass true to sort all descendant nodes */ sortChildren: function(cmp, deep) { var i,l, cl = this.children; if( !cl ){ return; } cmp = cmp || function(a, b) { var x = a.title.toLowerCase(), y = b.title.toLowerCase(); return x === y ? 0 : x > y ? 1 : -1; }; cl.sort(cmp); if( deep ){ for(i=0, l=cl.length; i"; }, /** Call fn(node) for all child nodes.
      * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".
      * Return false if iteration was stopped. * * @param {function} fn the callback function. * Return false to stop iteration, return "skip" to skip this node and children only. * @param {boolean} [includeSelf=false] * @returns {boolean} */ visit: function(fn, includeSelf) { var i, l, res = true, children = this.children; if( includeSelf === true ) { res = fn(this); if( res === false || res === "skip" ){ return res; } } if(children){ for(i=0, l=children.length; i * Stop iteration, if fn() returns false.
      * Return false if iteration was stopped. * * @param {function} fn the callback function. * Return false to stop iteration, return "skip" to skip this node and children only. * @param {boolean} [includeSelf=false] * @returns {boolean} */ visitParents: function(fn, includeSelf) { // Visit parent nodes (bottom up) if(includeSelf && fn(this) === false){ return false; } var p = this.parent; while( p ) { if(fn(p) === false){ return false; } p = p.parent; } return true; }, /** Write warning to browser console (prepending node info) * * @param {*} msg string or object or array of such */ warn: function(msg){ Array.prototype.unshift.call(arguments, this.toString()); consoleApply("warn", arguments); } }; /* ***************************************************************************** * Fancytree */ /** * Construct a new tree object. * * @class Fancytree * @classdesc The controller behind a fancytree. * This class also contains 'hook methods': see {@link Fancytree_Hooks}. * * @param {Widget} widget * * @property {FancytreeOptions} options * @property {FancytreeNode} rootNode * @property {FancytreeNode} activeNode * @property {FancytreeNode} focusNode * @property {jQueryObject} $div * @property {object} widget * @property {object} ext * @property {object} data * @property {object} options * @property {string} _id * @property {string} statusClassPropName * @property {string} ariaPropName * @property {string} nodeContainerAttrName * @property {string} $container * @property {FancytreeNode} lastSelectedNode */ function Fancytree(widget) { this.widget = widget; this.$div = widget.element; this.options = widget.options; if( this.options && $.isFunction(this.options.lazyload) ) { if( ! $.isFunction(this.options.lazyLoad ) ) { this.options.lazyLoad = function() { FT.warn("The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead."); widget.options.lazyload.apply(this, arguments); }; } } this.ext = {}; // Active extension instances // allow to init tree.data.foo from
      this.data = _getElementDataAsDict(this.$div); this._id = $.ui.fancytree._nextId++; this._ns = ".fancytree-" + this._id; // append for namespaced events this.activeNode = null; this.focusNode = null; this._hasFocus = null; this.lastSelectedNode = null; this.systemFocusElement = null; this.statusClassPropName = "span"; this.ariaPropName = "li"; this.nodeContainerAttrName = "li"; // Remove previous markup if any this.$div.find(">ul.fancytree-container").remove(); // Create a node without parent. var fakeParent = { tree: this }, $ul; this.rootNode = new FancytreeNode(fakeParent, { title: "root", key: "root_" + this._id, children: null, expanded: true }); this.rootNode.parent = null; // Create root markup $ul = $("
        ", { "class": "ui-fancytree fancytree-container" }).appendTo(this.$div); this.$container = $ul; this.rootNode.ul = $ul[0]; if(this.options.debugLevel == null){ this.options.debugLevel = FT.debugLevel; } // Add container to the TAB chain // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant this.$container.attr("tabindex", this.options.tabbable ? "0" : "-1"); if(this.options.aria){ this.$container .attr("role", "tree") .attr("aria-multiselectable", true); } } Fancytree.prototype = /** @lends Fancytree# */{ /* Return a context object that can be re-used for _callHook(). * @param {Fancytree | FancytreeNode | EventData} obj * @param {Event} originalEvent * @param {Object} extra * @returns {EventData} */ _makeHookContext: function(obj, originalEvent, extra) { var ctx, tree; if(obj.node !== undefined){ // obj is already a context object if(originalEvent && obj.originalEvent !== originalEvent){ $.error("invalid args"); } ctx = obj; }else if(obj.tree){ // obj is a FancytreeNode tree = obj.tree; ctx = { node: obj, tree: tree, widget: tree.widget, options: tree.widget.options, originalEvent: originalEvent }; }else if(obj.widget){ // obj is a Fancytree ctx = { node: null, tree: obj, widget: obj.widget, options: obj.widget.options, originalEvent: originalEvent }; }else{ $.error("invalid args"); } if(extra){ $.extend(ctx, extra); } return ctx; }, /* Trigger a hook function: funcName(ctx, [...]). * * @param {string} funcName * @param {Fancytree|FancytreeNode|EventData} contextObject * @param {any} [_extraArgs] optional additional arguments * @returns {any} */ _callHook: function(funcName, contextObject, _extraArgs) { var ctx = this._makeHookContext(contextObject), fn = this[funcName], args = Array.prototype.slice.call(arguments, 2); if(!$.isFunction(fn)){ $.error("_callHook('" + funcName + "') is not a function"); } args.unshift(ctx); // this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args); return fn.apply(this, args); }, /* Check if current extensions dependencies are met and throw an error if not. * * This method may be called inside the `treeInit` hook for custom extensions. * * @param {string} extension name of the required extension * @param {boolean} [required=true] pass `false` if the extension is optional, but we want to check for order if it is present * @param {boolean} [before] `true` if `name` must be included before this, `false` otherwise (use `null` if order doesn't matter) * @param {string} [message] optional error message (defaults to a descriptve error message) */ _requireExtension: function(name, required, before, message) { before = !!before; var thisName = this._local.name, extList = this.options.extensions, isBefore = $.inArray(name, extList) < $.inArray(thisName, extList), isMissing = required && this.ext[name] == null, badOrder = !isMissing && before != null && (before !== isBefore); _assert(thisName && thisName !== name); if( isMissing || badOrder ){ if( !message ){ if( isMissing || required ){ message = "'" + thisName + "' extension requires '" + name + "'"; if( badOrder ){ message += " to be registered " + (before ? "before" : "after") + " itself"; } }else{ message = "If used together, `" + name + "` must be registered " + (before ? "before" : "after") + " `" + thisName + "`"; } } $.error(message); return false; } return true; }, /** Activate node with a given key and fire focus and activate events. * * A prevously activated node will be deactivated. * If activeVisible option is set, all parents will be expanded as necessary. * Pass key = false, to deactivate the current node only. * @param {string} key * @returns {FancytreeNode} activated node (null, if not found) */ activateKey: function(key) { var node = this.getNodeByKey(key); if(node){ node.setActive(); }else if(this.activeNode){ this.activeNode.setActive(false); } return node; }, /** (experimental) * * @param {Array} patchList array of [key, NodePatch] arrays * @returns {$.Promise} resolved, when all patches have been applied * @see TreePatch */ applyPatch: function(patchList) { var dfd, i, p2, key, patch, node, patchCount = patchList.length, deferredList = []; for(i=0; i= 2 (prepending tree name) * * @param {*} msg string or object or array of such */ debug: function(msg){ if( this.options.debugLevel >= 2 ) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("debug", arguments); } }, // TODO: disable() // TODO: enable() // TODO: enableUpdate() // TODO: fromDict /** * Generate INPUT elements that can be submitted with html forms. * * In selectMode 3 only the topmost selected nodes are considered. * * @param {boolean | string} [selected=true] * @param {boolean | string} [active=true] */ generateFormElements: function(selected, active) { // TODO: test case var nodeList, selectedName = (selected !== false) ? "ft_" + this._id : selected, activeName = (active !== false) ? "ft_" + this._id + "_active" : active, id = "fancytree_result_" + this._id, $result = this.$container.find("div#" + id); if($result.length){ $result.empty(); }else{ $result = $("
        ", { id: id }).hide().appendTo(this.$container); } if(selectedName){ nodeList = this.getSelectedNodes( this.options.selectMode === 3 ); $.each(nodeList, function(idx, node){ $result.append($("", { type: "checkbox", name: selectedName, value: node.key, checked: true })); }); } if(activeName && this.activeNode){ $result.append($("", { type: "radio", name: activeName, value: this.activeNode.key, checked: true })); } }, /** * Return the currently active node or null. * @returns {FancytreeNode} */ getActiveNode: function() { return this.activeNode; }, /** Return the first top level node if any (not the invisible root node). * @returns {FancytreeNode | null} */ getFirstChild: function() { return this.rootNode.getFirstChild(); }, /** * Return node that has keyboard focus. * @param {boolean} [ifTreeHasFocus=false] (not yet implemented) * @returns {FancytreeNode} */ getFocusNode: function(ifTreeHasFocus) { // TODO: implement ifTreeHasFocus return this.focusNode; }, /** * Return node with a given key or null if not found. * @param {string} key * @param {FancytreeNode} [searchRoot] only search below this node * @returns {FancytreeNode | null} */ getNodeByKey: function(key, searchRoot) { // Search the DOM by element ID (assuming this is faster than traversing all nodes). // $("#...") has problems, if the key contains '.', so we use getElementById() var el, match; if(!searchRoot){ el = document.getElementById(this.options.idPrefix + key); if( el ){ return el.ftnode ? el.ftnode : null; } } // Not found in the DOM, but still may be in an unrendered part of tree // TODO: optimize with specialized loop // TODO: consider keyMap? searchRoot = searchRoot || this.rootNode; match = null; searchRoot.visit(function(node){ // window.console.log("getNodeByKey(" + key + "): ", node.key); if(node.key === key) { match = node; return false; } }, true); return match; }, // TODO: getRoot() /** * Return an array of selected nodes. * @param {boolean} [stopOnParents=false] only return the topmost selected * node (useful with selectMode 3) * @returns {FancytreeNode[]} */ getSelectedNodes: function(stopOnParents) { var nodeList = []; this.rootNode.visit(function(node){ if( node.selected ) { nodeList.push(node); if( stopOnParents === true ){ return "skip"; // stop processing this branch } } }); return nodeList; }, /** Return true if the tree control has keyboard focus * @returns {boolean} */ hasFocus: function(){ return !!this._hasFocus; }, /** Write to browser console if debugLevel >= 1 (prepending tree name) * @param {*} msg string or object or array of such */ info: function(msg){ if( this.options.debugLevel >= 1 ) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("info", arguments); } }, /* TODO: isInitializing: function() { return ( this.phase=="init" || this.phase=="postInit" ); }, TODO: isReloading: function() { return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound; }, TODO: isUserEvent: function() { return ( this.phase=="userEvent" ); }, */ /** * Make sure that a node with a given ID is loaded, by traversing - and * loading - its parents. This method is ment for lazy hierarchies. * A callback is executed for every node as we go. * @example * tree.loadKeyPath("/_3/_23/_26/_27", function(node, status){ * if(status === "loaded") { * console.log("loaded intermiediate node " + node); * }else if(status === "ok") { * node.activate(); * } * }); * * @param {string | string[]} keyPathList one or more key paths (e.g. '/3/2_1/7') * @param {function} callback callback(node, status) is called for every visited node ('loading', 'loaded', 'ok', 'error') * @returns {$.Promise} */ loadKeyPath: function(keyPathList, callback, _rootNode) { var deferredList, dfd, i, path, key, loadMap, node, segList, root = _rootNode || this.rootNode, sep = this.options.keyPathSeparator, self = this; if(!$.isArray(keyPathList)){ keyPathList = [keyPathList]; } // Pass 1: handle all path segments for nodes that are already loaded // Collect distinct top-most lazy nodes in a map loadMap = {}; for(i=0; i