/*! * 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 2014-01-04T16:42 */ // 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
    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 // 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(); 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); // tree.nodeSetFocus(ctx); _goto(this); } else if( this.parent && this.parent.parent ) { // this.parent.setFocus(); _goto(this.parent); } break; case KC.RIGHT: if( !this.expanded && (this.children || this.lazy) ) { this.setExpanded(); // tree.nodeSetFocus(ctx); _goto(this); } else if( this.children && this.children.length ) { // this.children[0].setFocus(); _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; } }, /** * Discard and reload all children of a lazy node. * @param {Boolean} [discard=false] * @returns $.Promise */ lazyLoad: function(discard) { if(discard || this.hasChildren() === undefined){ this.discard(); } _assert(!$.isArray(this.children)); var source = this.tree._triggerNodeEvent("lazyload", this); _assert(typeof source !== "boolean", "lazyload event must return source in data.result"); return this.tree._callHook("nodeLoadChildren", this, source); }, /** * @see Fancytree#nodeRender */ render: function(force, deep) { return this.tree._callHook("nodeRender", this, force, deep); }, /** * @see Fancytree#nodeRenderTitle */ renderTitle: function() { return this.tree._callHook("nodeRenderTitle", this); }, /** * @see Fancytree#nodeRenderStatus */ renderStatus: function() { return this.tree._callHook("nodeRenderStatus", this); }, /** Remove this node (not allowed for root).*/ remove: function() { return this.parent.removeChild(this); }, /**Remove childNode from list of direct children.*/ removeChild: function(childNode) { return this.tree._callHook("nodeRemoveChild", this, childNode); }, /**Remove all child nodes (and descendents).*/ removeChildren: function() { return this.tree._callHook("nodeRemoveChildren", this); }, // TODO: resetLazy() /** Schedule activity for delayed execution (cancel any pending request). * scheduleAction('cancel') will only cancel a pending request (if any). */ 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 {FancytreeNode} [topNode=null] this node will remain visible in * any case, even if `this` is outside the scroll pane. * @returns $.Promise */ scrollIntoView: function(effects, topNode) { effects = (effects === true) ? {duration: 200, queue: false} : effects; var topNodeY, dfd = new $.Deferred(), nodeY = $(this.span).position().top, nodeHeight = $(this.span).height(), $container = this.tree.$container, scrollTop = $container[0].scrollTop, horzScrollHeight = Math.max(0, ($container.innerHeight() - $container[0].clientHeight)), // containerHeight = $container.height(), containerHeight = $container.height() - horzScrollHeight, newScrollTop = null; // console.log("horzScrollHeight: " + horzScrollHeight); // console.log("$container[0].scrollTop: " + $container[0].scrollTop); // console.log("$container[0].scrollHeight: " + $container[0].scrollHeight); // console.log("$container[0].clientHeight: " + $container[0].clientHeight); // console.log("$container.innerHeight(): " + $container.innerHeight()); // console.log("$container.height(): " + $container.height()); if(nodeY < 0){ newScrollTop = scrollTop + nodeY; }else if((nodeY + nodeHeight) > containerHeight){ newScrollTop = scrollTop + nodeY - containerHeight + nodeHeight; // If a topNode was passed, make sure that it is never scrolled // outside the upper border if(topNode){ topNodeY = topNode ? $(topNode.span).position().top : 0; if((nodeY - topNodeY) > containerHeight){ newScrollTop = scrollTop + nodeY; } } } if(newScrollTop !== null){ if(effects){ // TODO: resolve dfd after animation // var that = this; $container.animate({scrollTop: newScrollTop}, effects); }else{ $container[0].scrollTop = newScrollTop; dfd.resolveWith(this); } }else{ dfd.resolveWith(this); } return dfd.promise(); /* from jQuery.menu: var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; if ( this._hasScroll() ) { borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0; paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0; offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; scroll = this.activeMenu.scrollTop(); elementHeight = this.activeMenu.height(); itemHeight = item.height(); if ( offset < 0 ) { this.activeMenu.scrollTop( scroll + offset ); } else if ( offset + itemHeight > elementHeight ) { this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); } } */ }, /**Activate this node. * @param {Boolean} [flag=true] pass false to deactivate */ setActive: function(flag){ return this.tree._callHook("nodeSetActive", this, flag); }, /**Expand this node. * @param {Boolean} [flag=true] pass false to collapse */ setExpanded: function(flag){ return this.tree._callHook("nodeSetExpanded", this, flag); }, /**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. * @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} [cmd] custom compare function. * @param {Boolean} [deep] 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'. * @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} false, if the iterator was stopped. */ 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; iul.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 }); 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); }, /** Activate node with a given key. * * A prevously activated node will be deactivated. * 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; }, /** * * @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 info) * * @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 node that is active. * @returns {FancytreeNode} */ getActiveNode: function() { return this.activeNode; }, /** @returns {FancytreeNode | null}*/ getFirstChild: function() { return this.rootNode.getFirstChild(); }, /** * Return node that has keyboard focus. * @param {Boolean} [ifTreeHasFocus=false] * @returns {FancytreeNode} */ getFocusNode: function(ifTreeHasFocus) { // TODO: implement ifTreeHasFocus return this.focusNode; }, /** * Return node with a given key. * @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 a list 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; }, /** * @returns {Boolean} true if the tree control has keyboard focus */ hasFocus: function(){ return !!this._hasFocus; }, /** Write to browser console if debugLevel >= 1 (prepending tree info) * * @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" ); }, */ /** * Expand all parents of one or more nodes. * Calls * @param {String | String[]} keyPath one or more key paths (e.g. '/3/2_1/7') * @param {function} callback callbeck(mode) is called for every visited node * @returns {$.Promise} */ /* _loadKeyPath: function(keyPath, callback) { var tree = this.tree; tree.logDebug("%s._loadKeyPath(%s)", this, keyPath); if(keyPath === ""){ throw "Key path must not be empty"; } var segList = keyPath.split(tree.options.keyPathSeparator); if(segList[0] === ""){ throw "Key path must be relative (don't start with '/')"; } var seg = segList.shift(); for(var i=0, l=this.childList.length; i < l; i++){ var child = this.childList[i]; if( child.data.key === seg ){ if(segList.length === 0) { // Found the end node callback.call(tree, child, "ok"); }else if(child.data.isLazy && (child.childList === null || child.childList === undefined)){ tree.logDebug("%s._loadKeyPath(%s) -> reloading %s...", this, keyPath, child); var self = this; child.reloadChildren(function(node, isOk){ // After loading, look for direct child with that key if(isOk){ tree.logDebug("%s._loadKeyPath(%s) -> reloaded %s.", node, keyPath, node); callback.call(tree, child, "loaded"); node._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback); }else{ tree.logWarning("%s._loadKeyPath(%s) -> reloadChildren() failed.", self, keyPath); callback.call(tree, child, "error"); } }); // Note: this line gives a JSLint warning (Don't make functions within a loop) // we can ignore it, since it will only be exectuted once, the the loop is ended // See also http://stackoverflow.com/questions/3037598/how-to-get-around-the-jslint-error-dont-make-functions-within-a-loop } else { callback.call(tree, child, "loaded"); // Look for direct child with that key child._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback); } return; } } // Could not find key tree.logWarning("Node not found: " + seg); return; }, */ /** * Expand all parents of one or more nodes. * Calls * @param {String | String[]} keyPathList one or more key paths (e.g. '/3/2_1/7') * @param {function} callback callbeck(mode) is called for every visited node ('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 doesn't steal focus (see table sample) if( targetType === "expander" ) { // Clicking the expander icon always expands/collapses this._callHook("nodeToggleExpanded", ctx); // this._callHook("nodeSetFocus", ctx, true); // issue 95 } else if( targetType === "checkbox" ) { // Clicking the checkbox always (de)selects this._callHook("nodeToggleSelected", ctx); this._callHook("nodeSetFocus", ctx, true); // issue 95 } else { // Honor `clickFolderMode` for expand = false; activate = true; if( node.folder ) { switch( ctx.options.clickFolderMode ) { case 2: // expand only expand = true; activate = false; break; case 3: // expand and activate activate = true; expand = true; //!node.isExpanded(); break; // else 1 or 4: just activate } } if( activate ) { this.nodeSetFocus(ctx); this._callHook("nodeSetActive", ctx, true); } if( expand ) { if(!activate){ // this._callHook("nodeSetFocus", ctx); } // this._callHook("nodeSetExpanded", ctx, true); this._callHook("nodeToggleExpanded", ctx); } } // Make sure that clicks stop, otherwise jumps to the top if(event.target.localName === "a" && event.target.className === "fancytree-title"){ event.preventDefault(); } // TODO: return promise? }, nodeCollapseSiblings: function(ctx) { // TODO: return promise? var ac, i, l, node = ctx.node; if( node.parent ){ ac = node.parent.children; for (i=0, l=ac.length; i= 0); // Unlink to support GC childNode.visit(function(n){ n.parent = null; }, true); if ( opts.removeNode ){ opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx); } // remove from child list children.splice(idx, 1); }, /**Remove HTML markup for all descendents of ctx.node. * @param {EventData} ctx */ nodeRemoveChildMarkup: function(ctx) { var node = ctx.node; FT.debug("nodeRemoveChildMarkup()", node.toString()); // TODO: Unlink attr.ftnode to support GC if(node.ul){ $(node.ul).remove(); node.visit(function(n){ n.li = n.ul = null; }); node.ul = null; } }, /**Remove all descendants of ctx.node. * @param {EventData} ctx */ nodeRemoveChildren: function(ctx) { var subCtx, node = ctx.node, children = node.children, opts = ctx.options; FT.debug("nodeRemoveChildren()", node.toString()); if(!children){ return; } if( this.activeNode && this.activeNode.isDescendantOf(node)){ this.activeNode.setActive(false); // TODO: don't fire events } if( this.focusNode && this.focusNode.isDescendantOf(node)){ this.focusNode = null; } // TODO: persist must take care to clear select and expand cookies this.nodeRemoveChildMarkup(ctx); // Unlink children to support GC // TODO: also delete this.children (not possible using visit()) subCtx = $.extend({}, ctx); node.visit(function(n){ n.parent = null; if ( opts.removeNode ){ subCtx.node = n; opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx); } }); // Set to 'undefined' which is interpreted as 'not yet loaded' for lazy nodes node.children = undefined; // TODO: ? this._isLoading = false; this.nodeRenderStatus(ctx); }, /**Remove HTML markup for ctx.node and all its descendents. * @param {EventData} ctx */ nodeRemoveMarkup: function(ctx) { var node = ctx.node; FT.debug("nodeRemoveMarkup()", node.toString()); // TODO: Unlink attr.ftnode to support GC if(node.li){ $(node.li).remove(); node.li = null; } this.nodeRemoveChildMarkup(ctx); }, /** * Create `
    • .. ..
    • ` tags for this node. * * This method takes care that all HTML markup is created that is required * to display this node in it's current state. * * Call this method to create new nodes, or after the strucuture * was changed (e.g. after moving this node or adding/removing children) * nodeRenderTitle() and nodeRenderStatus() are implied. * * Note: if a node was created/removed, nodeRender() must be called for the * parent! * *
    • * * * // only present in checkbox mode * * Node 1 * *
        // only present if node has children *
      • child1 ...
      • *
      • child2 ...
      • *
      *
    • *
      * * @param: {EventData} ctx * @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 * @param: {Boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later */ nodeRender: function(ctx, force, deep, collapsed, _recursive) { /* This method must take care of all cases where the current data mode * (i.e. node hierarchy) does not match the current markup. * * - node was not yet rendered: * create markup * - node was rendered: exit fast * - children have been added * - childern have been removed */ var childLI, childNode1, childNode2, i, l, subCtx, node = ctx.node, tree = ctx.tree, opts = ctx.options, aria = opts.aria, firstTime = false, parent = node.parent, isRootNode = !parent, children = node.children; // FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString()); if( ! isRootNode && ! parent.ul ) { // issue #105: calling node.collapse on a deep, unrendered node return; } _assert(isRootNode || parent.ul, "parent UL must exist"); // Render the node if( !isRootNode ){ // Discard markup on force-mode, or if it is not linked to parent
        if(node.li && (force || (node.li.parentNode !== node.parent.ul) ) ){ if(node.li.parentNode !== node.parent.ul){ // alert("unlink " + node + " (must be child of " + node.parent + ")"); this.warn("unlink " + node + " (must be child of " + node.parent + ")"); } // this.debug("nodeRemoveMarkup..."); this.nodeRemoveMarkup(ctx); } // Create
      • // node.debug("render..."); if( !node.li ) { // node.debug("render... really"); firstTime = true; node.li = document.createElement("li"); node.li.ftnode = node; if(aria){ // TODO: why doesn't this work: // node.li.role = "treeitem"; // $(node.li).attr("role", "treeitem") // .attr("aria-labelledby", "ftal_" + node.key); } if( node.key && opts.generateIds ){ node.li.id = opts.idPrefix + node.key; } node.span = document.createElement("span"); node.span.className = "fancytree-node"; if(aria){ $(node.span).attr("aria-labelledby", "ftal_" + node.key); } node.li.appendChild(node.span); // Note: we don't add the LI to the DOM know, but only after we // added all sub elements (hoping that this performs better since // the browser only have to render once) // TODO: benchmarks to prove this // parent.ul.appendChild(node.li); // Create inner HTML for the (expander, checkbox, icon, and title) this.nodeRenderTitle(ctx); // Allow tweaking and binding, after node was created for the first time // tree._triggerNodeEvent("createNode", ctx); if ( opts.createNode ){ opts.createNode.call(tree, {type: "createNode"}, ctx); } }else{ // this.nodeRenderTitle(ctx); } // Allow tweaking after node state was rendered // tree._triggerNodeEvent("renderNode", ctx); if ( opts.renderNode ){ opts.renderNode.call(tree, {type: "renderNode"}, ctx); } } // Visit child nodes if( children ){ if( isRootNode || node.expanded || deep === true ) { // Create a UL to hold the children if( !node.ul ){ node.ul = document.createElement("ul"); if((collapsed === true && !_recursive) || !node.expanded){ // hide top UL, so we can use an animation to show it later node.ul.style.display = "none"; } if(aria){ $(node.ul).attr("role", "group"); } if ( node.li ) { // issue #67 node.li.appendChild(node.ul); } else { node.tree.$div.append(node.ul); } } // Add child markup for(i=0, l=children.length; i order matches node.children order. // this.nodeFixOrder(ctx); childLI = node.ul.firstChild; for(i=0, l=children.length-1; i (expander, checkbox, icon, and title). * @param {EventData} ctx */ nodeRenderTitle: function(ctx, title) { // set node connector images, links and text var id, imageSrc, nodeTitle, role, tooltip, node = ctx.node, tree = ctx.tree, opts = ctx.options, aria = opts.aria, level = node.getLevel(), ares = [], icon = node.data.icon; if(title !== undefined){ node.title = title; } if(!node.span){ // Silently bail out if node was not rendered yet, assuming // node.render() will be called as the node becomes visible return; } // connector (expanded, expandable or simple) // TODO: optiimize this if clause if( level < opts.minExpandLevel ) { if(level > 1){ if(aria){ ares.push(""); }else{ ares.push(""); } } // .. else (i.e. for root level) skip expander/connector alltogether } else { if(aria){ ares.push(""); }else{ ares.push(""); } } // Checkbox mode if( opts.checkbox && node.hideCheckbox !== true && !node.isStatusNode ) { if(aria){ ares.push(""); }else{ ares.push(""); } } // folder or doctype icon role = aria ? " role='img'" : ""; if ( icon && typeof icon === "string" ) { imageSrc = (icon.charAt(0) === "/") ? icon : (opts.imagePath + icon); ares.push(""); } else if ( node.data.iconclass ) { // TODO: review and test and document ares.push(""); } else if ( icon === true || (icon !== false && opts.icons !== false) ) { // opts.icons defines the default behavior. // node.icon == true/false can override this ares.push(""); } // node title nodeTitle = ""; // TODO: currently undocumented; may be removed? if ( opts.renderTitle ){ nodeTitle = opts.renderTitle.call(tree, {type: "renderTitle"}, ctx) || ""; } if(!nodeTitle){ // TODO: escape tooltip string tooltip = node.tooltip ? " title='" + node.tooltip.replace(/\"/g, """) + "'" : ""; id = aria ? " id='ftal_" + node.key + "'" : ""; role = aria ? " role='treeitem'" : ""; // href = node.data.href || "#"; // if( opts.nolink || node.nolink ) { // nodeTitle = "" + node.title + ""; nodeTitle = "" + node.title + ""; // } else { // nodeTitle = "" + node.title + ""; // } } ares.push(nodeTitle); // Note: this will trigger focusout, if node had the focus node.span.innerHTML = ares.join(""); }, /** Update element classes according to node state. * @param {EventData} ctx */ nodeRenderStatus: function(ctx) { // Set classes for current status var node = ctx.node, tree = ctx.tree, opts = ctx.options, // nodeContainer = node[tree.nodeContainerAttrName], hasChildren = node.hasChildren(), isLastSib = node.isLastSibling(), aria = opts.aria, // $ariaElem = aria ? $(node[tree.ariaPropName]) : null, $ariaElem = $(node.span).find(".fancytree-title"), cn = opts._classNames, cnList = [], statusElem = node[tree.statusClassPropName]; if( !statusElem ){ // if this function is called for an unrendered node, ignore it (will be updated on nect render anyway) return; } // Build a list of class names that we will add to the node cnList.push(cn.node); if( tree.activeNode === node ){ cnList.push(cn.active); // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); // tree.$container.removeAttr("tabindex"); }else{ // $(">span.fancytree-title", statusElem).removeAttr("tabindex"); // tree.$container.attr("tabindex", "0"); } if( tree.focusNode === node ){ cnList.push(cn.focused); if(aria){ // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); // $(">span.fancytree-title", statusElem).attr("tabindex", "-1"); // TODO: is this the right element for this attribute? $ariaElem .attr("aria-activedescendant", true); // .attr("tabindex", "-1"); } }else if(aria){ // $(">span.fancytree-title", statusElem).attr("tabindex", "-1"); $ariaElem .removeAttr("aria-activedescendant"); // .removeAttr("tabindex"); } if( node.expanded ){ cnList.push(cn.expanded); if(aria){ $ariaElem.attr("aria-expanded", true); } }else if(aria){ $ariaElem.removeAttr("aria-expanded"); } if( node.folder ){ cnList.push(cn.folder); } if( hasChildren !== false ){ cnList.push(cn.hasChildren); } // TODO: required? if( isLastSib ){ cnList.push(cn.lastsib); } if( node.lazy && node.children == null ){ cnList.push(cn.lazy); } if( node.partsel ){ cnList.push(cn.partsel); } if( node.selected ){ cnList.push(cn.selected); if(aria){ $ariaElem.attr("aria-selected", true); } }else if(aria){ $ariaElem.attr("aria-selected", false); } if( node.extraClasses ){ cnList.push(node.extraClasses); } // IE6 doesn't correctly evaluate multiple class names, // so we create combined class names that can be used in the CSS if( hasChildren === false ){ cnList.push(cn.combinedExpanderPrefix + "n" + (isLastSib ? "l" : "") ); }else{ cnList.push(cn.combinedExpanderPrefix + (node.expanded ? "e" : "c") + (node.lazy && node.children == null ? "d" : "") + (isLastSib ? "l" : "") ); } cnList.push(cn.combinedIconPrefix + (node.expanded ? "e" : "c") + (node.folder ? "f" : "") ); // node.span.className = cnList.join(" "); statusElem.className = cnList.join(" "); // TODO: we should not set this in the tag also, if we set it here: // Maybe most (all) of the classes should be set in LI instead of SPAN? if(node.li){ node.li.className = isLastSib ? cn.lastsib : ""; } }, /** Activate node. * flag defaults to true. * If flag is true, the node is activated (must be a synchronous operation) * If flag is false, the node is deactivated (must be a synchronous operation) * @param {EventData} ctx * @param {Boolean} [flag=true] */ nodeSetActive: function(ctx, flag) { // Handle user click / [space] / [enter], according to clickFolderMode. var subCtx, node = ctx.node, tree = ctx.tree, opts = ctx.options, // userEvent = !!ctx.originalEvent, isActive = (node === tree.activeNode); // flag defaults to true flag = (flag !== false); node.debug("nodeSetActive", flag); if(isActive === flag){ // Nothing to do return _getResolvedPromise(node); }else if(flag && this._triggerNodeEvent("beforeActivate", node, ctx.originalEvent) === false ){ // Callback returned false return _getRejectedPromise(node, ["rejected"]); } if(flag){ if(tree.activeNode){ _assert(tree.activeNode !== node, "node was active (inconsistency)"); subCtx = $.extend({}, ctx, {node: tree.activeNode}); tree.nodeSetActive(subCtx, false); _assert(tree.activeNode === null, "deactivate was out of sync?"); } if(opts.activeVisible){ tree.nodeMakeVisible(ctx); } tree.activeNode = node; tree.nodeRenderStatus(ctx); tree.nodeSetFocus(ctx); tree._triggerNodeEvent("activate", node); }else{ _assert(tree.activeNode === node, "node was not active (inconsistency)"); tree.activeNode = null; this.nodeRenderStatus(ctx); ctx.tree._triggerNodeEvent("deactivate", node); } }, /** Expand or collapse node, return Deferred.promise. * * @param {EventData} ctx * @param {Boolean} [flag=true] * @returns {$.Promise} The deferred will be resolved as soon as the (lazy) * data was retrieved, rendered, and the expand animation finshed. */ nodeSetExpanded: function(ctx, flag) { var _afterLoad, dfd, i, l, parents, prevAC, node = ctx.node, tree = ctx.tree, opts = ctx.options; // flag defaults to true flag = (flag !== false); node.debug("nodeSetExpanded(" + flag + ")"); // TODO: !!node.expanded is nicer, but doesn't pass jshint // https://github.com/jshint/jshint/issues/455 // if( !!node.expanded === !!flag){ if((node.expanded && flag) || (!node.expanded && !flag)){ // Nothing to do node.debug("nodeSetExpanded(" + flag + "): nothing to do"); return _getResolvedPromise(node); }else if(flag && !node.lazy && !node.hasChildren() ){ // Prevent expanding of empty nodes return _getRejectedPromise(node, ["empty"]); }else if( !flag && node.getLevel() < opts.minExpandLevel ) { // Prevent collapsing locked levels return _getRejectedPromise(node, ["locked"]); }else if ( this._triggerNodeEvent("beforeExpand", node, ctx.originalEvent) === false ){ // Callback returned false return _getRejectedPromise(node, ["rejected"]); } // dfd = new $.Deferred(); // Auto-collapse mode: collapse all siblings if( flag && !node.expanded && opts.autoCollapse ) { parents = node.getParentList(false, true); prevAC = opts.autoCollapse; try{ opts.autoCollapse = false; for(i=0, l=parents.length; iul.fancytree-container").empty(); // TODO: call destructors and remove reference loops tree.rootNode.children = null; }, /** Widget was created (called only once, even it re-initialized). * @param {EventData} ctx */ treeCreate: function(ctx) { }, /** Widget was destroyed. * @param {EventData} ctx */ treeDestroy: function(ctx) { }, /** Widget was (re-)initialized. * @param {EventData} ctx */ treeInit: function(ctx) { //this.debug("Fancytree.treeInit()"); this.treeLoad(ctx); }, /** Parse Fancytree from source, as configured in the options. * @param {EventData} ctx * @param {object} [source] new source */ treeLoad: function(ctx, source) { var type, $ul, tree = ctx.tree, $container = ctx.widget.element, dfd, // calling context for root node rootCtx = $.extend({}, ctx, {node: this.rootNode}); if(tree.rootNode.children){ this.treeClear(ctx); } source = source || this.options.source; if(!source){ type = $container.data("type") || "html"; switch(type){ case "html": $ul = $container.find(">ul:first"); $ul.addClass("ui-fancytree-source ui-helper-hidden"); source = $.ui.fancytree.parseHtml($ul); break; case "json": // $().addClass("ui-helper-hidden"); source = $.parseJSON($container.text()); if(source.children){ if(source.title){tree.title = source.title;} source = source.children; } break; default: $.error("Invalid data-type: " + type); } }else if(typeof source === "string"){ // TODO: source is an element ID _raiseNotImplemented(); } // $container.addClass("ui-widget ui-widget-content ui-corner-all"); // Trigger fancytreeinit after nodes have been loaded dfd = this.nodeLoadChildren(rootCtx, source).done(function(){ tree.render(); if( ctx.options.selectMode === 3 ){ tree.rootNode.fixSelection3FromEndNodes(); } tree._triggerTreeEvent("init", true); }).fail(function(){ tree.render(); tree._triggerTreeEvent("init", false); }); return dfd; }, treeSetFocus: function(ctx, flag, _calledByNodeSetFocus) { flag = (flag !== false); this.debug("treeSetFocus(" + flag + "), _calledByNodeSetFocus: " + _calledByNodeSetFocus); this.debug(" focusNode: " + this.focusNode); this.debug(" activeNode: " + this.activeNode); if( flag !== this.hasFocus() ){ this._hasFocus = flag; this.$container.toggleClass("fancytree-treefocus", flag); this._triggerTreeEvent(flag ? "focusTree" : "blurTree"); } }, /** Re-fire beforeActivate and activate events. */ reactivate: function(setFocus) { var node = this.activeNode; if( node ) { this.activeNode = null; // Force re-activating node.setActive(); if( setFocus ){ node.setFocus(); } } }, // TODO: redraw() /** Reload tree from source and return a promise. * @param source * @returns {$.Promise} */ reload: function(source) { this._callHook("treeClear", this); return this._callHook("treeLoad", this, source); }, /**Render tree (i.e. all top-level nodes). * @param {Boolean} [force=false] * @param {Boolean} [deep=false] */ render: function(force, deep) { return this.rootNode.render(force, deep); }, // TODO: selectKey: function(key, select) // TODO: serializeArray: function(stopOnParents) /** * @param {Boolean} [flag=true] */ setFocus: function(flag) { // _assert(false, "Not implemented"); return this._callHook("treeSetFocus", this, flag); }, /** * Return all nodes as nested list of {@link NodeData}. * * @param {Boolean} [includeRoot=false] Returns the hidden system root node (and it's children) * @param {function} [callback] Called for every node * @returns {Array | object} * @see FancytreeNode#toDict */ toDict: function(includeRoot, callback){ var res = this.rootNode.toDict(true, callback); return includeRoot ? res : res.children; }, /**Implicitly called for string conversions. * @returns {String} */ toString: function(){ return ""; }, /** _trigger a widget event with additional node ctx. * @see EventData */ _triggerNodeEvent: function(type, node, originalEvent, extra) { // this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx); var ctx = this._makeHookContext(node, originalEvent, extra), res = this.widget._trigger(type, originalEvent, ctx); if(res !== false && ctx.result !== undefined){ return ctx.result; } return res; }, /** _trigger a widget event with additional tree data. */ _triggerTreeEvent: function(type, originalEvent) { // this.debug("_trigger(" + type + ")", ctx); var ctx = this._makeHookContext(this, originalEvent), res = this.widget._trigger(type, originalEvent, ctx); if(res !== false && ctx.result !== undefined){ return ctx.result; } return res; }, /** Call fn(node) for all nodes. * * @param {function} fn the callback function. * Return false to stop iteration, return "skip" to skip this node and children only. * @returns {Boolean} false, if the iterator was stopped. */ visit: function(fn) { return this.rootNode.visit(fn, false); }, /** Write warning to browser console (prepending tree info) * * @param {*} msg string or object or array of such */ warn: function(msg){ Array.prototype.unshift.call(arguments, this.toString()); consoleApply("warn", arguments); } }; /* ****************************************************************************** * jQuery UI widget boilerplate * @ name ui_fancytree * @ class The jQuery.ui.fancytree widget */ /* * @namespace ui */ /* * @namespace ui.fancytree */ /** @namespace $.ui.fancytree */ $.widget("ui.fancytree", /** @lends $.ui.fancytree.prototype */ { /**These options will be used as defaults * @type {FancytreeOptions} */ options: { /** @type {Boolean} Make sure, active nodes are visible (expanded). */ activeVisible: true, ajax: { type: "GET", cache: false, // false: Append random '_' argument to the request url to prevent caching. // timeout: 0, // >0: Make sure we get an ajax error if server is unreachable dataType: "json" // Expect json format and pass json object to callbacks. }, // aria: false, // TODO: default to true autoActivate: true, autoCollapse: false, // autoFocus: false, autoScroll: false, checkbox: false, /**defines click behavior*/ clickFolderMode: 4, debugLevel: null, // 0..2 (null: use global setting $.ui.fancytree.debugInfo) disabled: false, // TODO: required anymore? enableAspx: true, // TODO: document extensions: [], fx: { height: "toggle", duration: 200 }, generateIds: false, icons: true, idPrefix: "ft_", keyboard: true, keyPathSeparator: "/", minExpandLevel: 1, selectMode: 2, strings: { loading: "Loading…", loadError: "Load error!" }, tabbable: true, _classNames: { node: "fancytree-node", folder: "fancytree-folder", combinedExpanderPrefix: "fancytree-exp-", combinedIconPrefix: "fancytree-ico-", hasChildren: "fancytree-has-children", active: "fancytree-active", selected: "fancytree-selected", expanded: "fancytree-expanded", lazy: "fancytree-lazy", focused: "fancytree-focused", partsel: "fancytree-partsel", lastsib: "fancytree-lastsib", loading: "fancytree-loading", error: "fancytree-error" }, // events lazyload: null, postProcess: null }, /* Set up the widget, Called on first $().fancytree() */ _create: function() { this.tree = new Fancytree(this); this.$source = this.source || this.element.data("type") === "json" ? this.element : this.element.find(">ul:first"); // Subclass Fancytree instance with all enabled extensions var extension, extName, i, extensions = this.options.extensions, base = this.tree; for(i=0; i"); if(callDefault){ // In jQuery UI 1.8, you have to manually invoke the _setOption method from the base widget $.Widget.prototype._setOption.apply(this, arguments); // TODO: In jQuery UI 1.9 and above, you use the _super method instead // this._super( "_setOption", key, value ); } if(rerender){ this.tree.render(true, false); // force, not-deep } }, /** Use the destroy method to clean up any modifications your widget has made to the DOM */ destroy: function() { this._unbind(); this.tree._callHook("treeDestroy", this.tree); // this.element.removeClass("ui-widget ui-widget-content ui-corner-all"); this.tree.$div.find(">ul.fancytree-container").remove(); this.$source && this.$source.removeClass("ui-helper-hidden"); // In jQuery UI 1.8, you must invoke the destroy method from the base widget $.Widget.prototype.destroy.call(this); // TODO: delete tree and nodes to make garbage collect easier? // TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method }, // ------------------------------------------------------------------------- /* Remove all event handlers for our namespace */ _unbind: function() { var ns = this.tree._ns; this.element.unbind(ns); this.tree.$container.unbind(ns); $(document).unbind(ns); }, /* Add mouse and kyboard handlers to the container */ _bind: function() { var that = this, opts = this.options, tree = this.tree, ns = tree._ns // selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" ) ; // Remove all previuous handlers for this tree this._unbind(); //alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container); // tree.debug("bind events; container: ", tree.$container); tree.$container.on("focusin" + ns + " focusout" + ns, function(event){ var node = FT.getNode(event), flag = (event.type === "focusin"); // tree.debug("Tree container got event " + event.type, node, event); // tree.treeOnFocusInOut.call(tree, event); if(node){ // For example clicking into an that is part of a node tree._callHook("nodeSetFocus", node, flag); }else{ tree._callHook("treeSetFocus", tree, flag); } }).on("selectstart" + ns, "span.fancytree-title", function(event){ // prevent mouse-drags to select text ranges // tree.debug(" got event " + event.type); event.preventDefault(); }).on("keydown" + ns, function(event){ // TODO: also bind keyup and keypress // tree.debug("got event " + event.type + ", hasFocus:" + tree.hasFocus()); // if(opts.disabled || opts.keyboard === false || !tree.hasFocus() ){ if(opts.disabled || opts.keyboard === false ){ return true; } var res, node = tree.focusNode, // node may be null ctx = tree._makeHookContext(node || tree, event), prevPhase = tree.phase; try { tree.phase = "userEvent"; // If a 'fancytreekeydown' handler returns false, skip the default // handling (implemented by tree.nodeKeydown()). if(node){ res = tree._triggerNodeEvent("keydown", node, event); }else{ res = tree._triggerTreeEvent("keydown", event); } if ( res === "preventNav" ){ res = true; // prevent keyboard navigation, but don't prevent default handling of embedded input controls } else if ( res !== false ){ res = tree._callHook("nodeKeydown", ctx); } return res; } finally { tree.phase = prevPhase; } }).on("click" + ns + " dblclick" + ns, function(event){ if(opts.disabled){ return true; } var ctx, et = FT.getEventTarget(event), node = et.node, tree = that.tree, prevPhase = tree.phase; if( !node ){ return true; // Allow bubbling of other events } ctx = tree._makeHookContext(node, event); // that.tree.debug("event(" + event.type + "): node: ", node); try { tree.phase = "userEvent"; switch(event.type) { case "click": ctx.targetType = et.type; return ( tree._triggerNodeEvent("click", ctx, event) === false ) ? false : tree._callHook("nodeClick", ctx); case "dblclick": ctx.targetType = et.type; return ( tree._triggerNodeEvent("dblclick", ctx, event) === false ) ? false : tree._callHook("nodeDblclick", ctx); } // } catch(e) { // // var _ = null; // issue 117 // TODO // $.error(e); } finally { tree.phase = prevPhase; } }); }, /** @returns {FancytreeNode} the active node or null */ getActiveNode: function() { return this.tree.activeNode; }, /** * @param {String} key * @returns {FancytreeNode} the matching node or null */ getNodeByKey: function(key) { return this.tree.getNodeByKey(key); }, /** @returns {FancytreeNode} the invisible system root node */ getRootNode: function() { return this.tree.rootNode; }, /** @returns {Fancytree} the current tree instance */ getTree: function() { return this.tree; } }); // $.ui.fancytree was created by the widget factory. Create a local shortcut: FT = $.ui.fancytree; /** * Static members in the `$.ui.fancytree` namespace. * @ name $.ui.fancytree * @example: * alert(""version: " + $.ui.fancytree.version); * var node = $.ui.fancytree.() */ $.extend($.ui.fancytree, /** @lends $.ui.fancytree */ { /** @type {String} */ version: "2.0.0-5", /** @type {String} */ buildType: "release", /** @type {int} */ debugLevel: 1, // used by $.ui.fancytree.debug() and as default for tree.options.debugLevel _nextId: 1, _nextNodeKey: 1, _extensions: {}, // focusTree: null, /** Expose class object as $.ui.fancytree._FancytreeClass */ _FancytreeClass: Fancytree, /** Expose class object as $.ui.fancytree._FancytreeNodeClass */ _FancytreeNodeClass: FancytreeNode, /* Feature checks to provide backwards compatibility */ jquerySupports: { // http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at positionMyOfs: isVersionAtLeast($.ui.version, 1, 9) }, assert: function(cond, msg){ return _assert(cond, msg); }, debug: function(msg){ /*jshint expr:true */ ($.ui.fancytree.debugLevel >= 2) && consoleApply("log", arguments); }, error: function(msg){ consoleApply("error", arguments); }, /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event. * * @static * @param {Event} event Mouse event, e.g. click, ... * @returns {String} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ getEventTargetType: function(event){ return this.getEventTarget(event).type; }, /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event. * * @param {Event} event Mouse event, e.g. click, ... * @returns {object} Return a {node: FancytreeNode, type: TYPE} object * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ getEventTarget: function(event){ var tcn = event && event.target ? event.target.className : "", res = {node: this.getNode(event.target), type: undefined}; // tcn may contains UI themeroller or Font Awesome classes, so we use // a fast version of $(res.node).hasClass() // See http://jsperf.com/test-for-classname/2 if( /\bfancytree-title\b/.test(tcn) ){ res.type = "title"; }else if( /\bfancytree-expander\b/.test(tcn) ){ res.type = (res.node.hasChildren() === false ? "prefix" : "expander"); }else if( /\bfancytree-checkbox\b/.test(tcn) ){ res.type = "checkbox"; }else if( /\bfancytree-icon\b/.test(tcn) ){ res.type = "icon"; }else if( /\bfancytree-node\b/.test(tcn) ){ // TODO: issue #93 (http://code.google.com/p/fancytree/issues/detail?id=93) // res.type = this._getTypeForOuterNodeEvent(event); res.type = "title"; } return res; }, /** Return a FancytreeNode instance from element. * * @param {Element | jQueryObject | Event} el * @returns {FancytreeNode} matching node or null */ getNode: function(el){ if(el instanceof FancytreeNode){ return el; // el already was a FancytreeNode }else if(el.selector !== undefined){ el = el[0]; // el was a jQuery object: use the DOM element }else if(el.originalEvent !== undefined){ el = el.target; // el was an Event } while( el ) { if(el.ftnode) { return el.ftnode; } el = el.parentNode; } return null; }, /* Return a Fancytree instance from element. * TODO: this function could help to get around the data('fancytree') / data('ui-fancytree') problem * @param {Element | jQueryObject | Event} el * @returns {Fancytree} matching tree or null * / getTree: function(el){ if(el instanceof Fancytree){ return el; // el already was a Fancytree }else if(el.selector !== undefined){ el = el[0]; // el was a jQuery object: use the DOM element }else if(el.originalEvent !== undefined){ el = el.target; // el was an Event } ... return null; }, */ info: function(msg){ /*jshint expr:true */ ($.ui.fancytree.debugLevel >= 1) && consoleApply("info", arguments); }, /** * Parse tree data from HTML