/*! * jquery.fancytree.js * Dynamic tree view control, with support for lazy loading of branches. * https://github.com/mar10/fancytree/ * * Copyright (c) 2006-2013, Martin Wendt (http://wwWendt.de) * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.0.0-5 * @date 2013-12-23T10:11 */ // 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); }; } // 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 //Boolean attributes that can be set with equivalent class names in the LI tags CLASS_ATTRS = "active expanded focus folder 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 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 tag this.isStatusNode = false; 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. * * @param {NodeData} node node definition * @param {String} [mode] 'before', 'after', or '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); }, /** * * @param {NodePatch} patch * @returns {$.Promise} * @see {@link applyPatch} to modify existing child nodes. * @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; }, /** * @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); } }, /** Remove all children of a lazy node and collapse.*/ discard: function(){ if(this.lazy && $.isArray(this.children)){ this.removeChildren(); return this.setExpanded(false); } }, // 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); 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