vendor/assets/javascripts/fancytree/jquery.fancytree-all.js in fancytree-rails-0.0.2 vs vendor/assets/javascripts/fancytree/jquery.fancytree-all.js in fancytree-rails-2.0.0.pre.6.pre.1

- old
+ new

@@ -1,18 +1,22 @@ /*! * 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) + * Copyright (c) 2006-2014, 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ +/** Core Fancytree module. + */ + + // Start of local namespace ;(function($, window, document, undefined) { "use strict"; // prevent duplicate loading @@ -172,11 +176,11 @@ }; } var i, FT = null, // initialized below - //Boolean attributes that can be set with equivalent class names in the LI tags + //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 = {}, @@ -191,33 +195,33 @@ * FancytreeNode */ /** - * Creates a new node - * @class Represents the hierarchical data model and operations. - * @name FancytreeNode - * @constructor + * Creates a new FancytreeNode + * + * @class FancytreeNode + * @classdesc A FancytreeNode represents the hierarchical data model and operations. + * * @param {FancytreeNode} parent - * @param {NodeData} data + * @param {NodeData} obj * * @property {Fancytree} tree * @property {FancytreeNode} parent Parent node - * @property {String} key - * @property {String} title + * @property {string} key + * @property {string} title * @property {object} data Contains all extra data that was passed on node creation * @property {FancytreeNode[] | null | undefined} children list of child nodes - * @property {Boolean} isStatusNode - * @property {Boolean} expanded - * @property {Boolean} folder - * @property {Boolean} href - * @property {String} extraClasses - * @property {Boolean} lazy - * @property {Boolean} nolink OBSOLETE - * @property {Boolean} selected - * @property {String} target - * @property {String} tooltip + * @property {boolean} isStatusNode + * @property {boolean} expanded + * @property {boolean} folder + * @property {string} extraClasses + * @property {boolean} lazy + * @property {boolean} selected + * @property {string} tooltip + * @property {string} data.href + * @property {string} data.target */ function FancytreeNode(parent, obj){ var i, l, name, cl; this.parent = parent; @@ -264,11 +268,11 @@ this._setChildren(cl); } } -FancytreeNode.prototype = /**@lends FancytreeNode*/{ +FancytreeNode.prototype = /** @lends FancytreeNode# */{ /* Return the direct child FancytreeNode with a given key, index. */ _findDirectChild: function(ptr){ var i, l, cl = this.children; @@ -299,20 +303,15 @@ }, /** * Append (or insert) a list of child nodes. * * @param {NodeData[]} children array of child node definitions (also single child accepted) - * @param {FancytreeNode | String | Integer} [insertBefore] child node (or key or index of such). + * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such). * If omitted, the new children are appended. * @returns {FancytreeNode} first child added * - * @see applyPatch to modify existing child nodes. - * @see FanctreeNode.applyPatch to modify existing child nodes. - * @see FanctreeNode#applyPatch to modify existing child nodes. - * @see applyPatch - * @see FanctreeNode.applyPatch - * @see FanctreeNode#applyPatch + * @see FancytreeNode#applyPatch */ addChildren: function(children, insertBefore){ var i, l, pos, firstNode = null, nodeList = []; @@ -346,12 +345,14 @@ 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] 'before', 'after', or 'child' + * @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"; @@ -366,14 +367,14 @@ return this.addChildren(node); } _assert(false, "Invalid mode: " + mode); }, /** + * Modify existing child nodes. * * @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){ @@ -423,20 +424,20 @@ 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 {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 + * @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 ){ @@ -469,11 +470,11 @@ } }, // 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 + * @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) { @@ -486,11 +487,11 @@ }); 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 + * @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 * <b>fat</b> text */ @@ -671,22 +672,22 @@ /** @returns {int} 0-based child index.*/ getIndex: function() { // return this.parent.children.indexOf(this); return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7 }, - /**@returns {String} hierarchical child index (1-based: '3.2.4').*/ + /**@returns {string} hierarchical child index (1-based: '3.2.4').*/ getIndexHier: function(separator) { separator = separator || "."; var res = []; $.each(this.getParentList(false, true), function(i, o){ res.push(o.getIndex() + 1); }); return res.join(separator); }, /** - * @param {Boolean} [excludeSelf=false] - * @returns {String} parent keys separated by options.keyPathSeparator + * @param {boolean} [excludeSelf=false] + * @returns {string} parent keys separated by options.keyPathSeparator */ getKeyPath: function(excludeSelf) { var path = [], sep = this.tree.options.keyPathSeparator; this.visitParents(function(n){ @@ -729,12 +730,12 @@ getParent: function() { // TODO: return null for top-level nodes? return this.parent; }, /** - * @param {Boolean} [includeRoot=false] - * @param {Boolean} [includeSelf=false] + * @param {boolean} [includeRoot=false] + * @param {boolean} [includeSelf=false] * @returns {FancytreeNode[]} */ getParentList: function(includeRoot, includeSelf) { var l = [], dtn = includeSelf ? this : this.parent; @@ -775,28 +776,28 @@ } return true; } return !!this.children; }, - /**@returns {Boolean} true, if node has keyboard focus*/ + /**@returns {boolean} true, if node has keyboard focus*/ hasFocus: function() { return (this.tree.hasFocus() && this.tree.focusNode === this); }, - /**@returns {Boolean} true, if node is active*/ + /**@returns {boolean} true, if node is active*/ isActive: function() { return (this.tree.activeNode === this); }, /** * @param {FancytreeNode} otherNode - * @returns {Boolean} true, if node is a direct child of otherNode + * @returns {boolean} true, if node is a direct child of otherNode */ isChildOf: function(otherNode) { return (this.parent && this.parent === otherNode); }, /** * @param {FancytreeNode} otherNode - * @returns {Boolean} true, if node is a sub node of otherNode + * @returns {boolean} true, if node is a sub node of otherNode */ isDescendantOf: function(otherNode) { if(!otherNode || otherNode.tree !== this.tree){ return false; } @@ -807,41 +808,41 @@ } p = p.parent; } return false; }, - /** @returns {Boolean} true, if node is expanded*/ + /** @returns {boolean} true, if node is expanded*/ isExpanded: function() { return !!this.expanded; }, - /** @returns {Boolean}*/ + /** @returns {boolean}*/ isFirstSibling: function() { var p = this.parent; return !p || p.children[0] === this; }, - /** @returns {Boolean}*/ + /** @returns {boolean}*/ isFolder: function() { return !!this.folder; }, - /** @returns {Boolean}*/ + /** @returns {boolean}*/ isLastSibling: function() { var p = this.parent; return !p || p.children[p.children.length-1] === this; }, - /** @returns {Boolean} true, if node is lazy (even if data was already loaded)*/ + /** @returns {boolean} true, if node is lazy (even if data was already loaded)*/ isLazy: function() { return !!this.lazy; }, - /** @returns {Boolean} true, if children are currently beeing loaded*/ + /** @returns {boolean} true, if children are currently beeing loaded*/ isLoading: function() { _raiseNotImplemented(); // TODO: implement }, - /**@returns {Boolean} true, if node is the (invisible) system root node*/ + /**@returns {boolean} true, if node is the (invisible) system root node*/ isRoot: function() { return (this.tree.rootNode === this); }, - /** @returns {Boolean} true, if node is selected (e.g. has a checkmark set)*/ + /** @returns {boolean} true, if node is selected (e.g. has a checkmark set)*/ isSelected: function() { return !!this.selected; }, // TODO: use _isStatusNode as class attribute name // isStatusNode: function() { @@ -870,11 +871,11 @@ parents[i].setExpanded(true); } }, /** Move this node to targetNode. * @param {FancytreeNode} targetNode - * @param {String} mode + * @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. @@ -891,11 +892,11 @@ if(this === targetNode){ return; }else if( !this.parent ){ throw "Cannot move system root"; }else if( targetParent.isDescendantOf(this) ){ - throw "Cannot move a node to it's own descendant"; + 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; @@ -961,14 +962,14 @@ // 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); - } + // 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(); } @@ -1029,10 +1030,16 @@ // 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 ) { @@ -1042,24 +1049,20 @@ } 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(); @@ -1087,11 +1090,11 @@ handled = false; } }, /** * Discard and reload all children of a lazy node. - * @param {Boolean} [discard=false] + * @param {boolean} [discard=false] * @returns $.Promise */ lazyLoad: function(discard) { if(discard || this.hasChildren() === undefined){ this.discard(); @@ -1100,23 +1103,23 @@ 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 + * @see Fancytree_Hooks#nodeRender */ render: function(force, deep) { return this.tree._callHook("nodeRender", this, force, deep); }, /** - * @see Fancytree#nodeRenderTitle + * @see Fancytree_Hooks#nodeRenderTitle */ renderTitle: function() { return this.tree._callHook("nodeRenderTitle", this); }, /** - * @see Fancytree#nodeRenderStatus + * @see Fancytree_Hooks#nodeRenderStatus */ renderStatus: function() { return this.tree._callHook("nodeRenderStatus", this); }, /** Remove this node (not allowed for root).*/ @@ -1163,11 +1166,11 @@ } // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer); }, /** * - * @param {Boolean | PlainObject} [effects=false] animation options. + * @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) { @@ -1197,11 +1200,11 @@ // 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; + newScrollTop = scrollTop + topNodeY; } } } if(newScrollTop !== null){ if(effects){ @@ -1234,31 +1237,34 @@ } */ }, /**Activate this node. - * @param {Boolean} [flag=true] pass false to deactivate + * @param {boolean} [flag=true] pass false to deactivate + * @param {object} [opts] additional options. Defaults to {noEvents: false} */ - setActive: function(flag){ - return this.tree._callHook("nodeSetActive", this, flag); + setActive: function(flag, opts){ + return this.tree._callHook("nodeSetActive", this, flag, opts); }, - /**Expand this node. - * @param {Boolean} [flag=true] pass false to collapse + /**Expand or collapse this node. + * @param {boolean} [flag=true] pass false to collapse + * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false} + * @returns {$.Promise} resolved, when lazy loading and animations are done */ - setExpanded: function(flag){ - return this.tree._callHook("nodeSetExpanded", this, flag); + 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 + * @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 + * @param {boolean} [flag=true] pass false to deselect */ setSelected: function(flag){ return this.tree._callHook("nodeSetSelected", this, flag); }, /**Rename this node. @@ -1267,12 +1273,12 @@ 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 + * @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; @@ -1294,25 +1300,24 @@ } if( deep !== "$norender$" ){ this.render(); } }, - /** Convert node (or whole branch) into a dictionary. + /** Convert node (or whole branch) into a plain object. * * The result is compatible with node.addChildren(). * - * @param {Boolean} recursive - * @param {function} callback callback(dict) is called for every dict (), in order to allow modifications - * @returns {NodePatch} + * @param {boolean} recursive + * @param {function} callback callback(dict) is called for every node, in order to allow modifications + * @returns {NodeData} */ toDict: function(recursive, callback) { var i, l, node, dict = {}, self = this; $.each(NODE_ATTRS, function(i, a){ -// if(self[a] !== undefined && self[a] !== null){ if(self[a] || self[a] === false){ dict[a] = self[a]; } }); if(!$.isEmptyObject(this.data)){ @@ -1352,12 +1357,12 @@ }, /** 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. + * @param {boolean} [includeSelf=false] + * @returns {boolean} false, if the iterator was stopped. */ visit: function(fn, includeSelf) { var i, l, res = true, children = this.children; @@ -1380,11 +1385,11 @@ }, /** * * @param fn * @param includeSelf - * @returns {Boolean} + * @returns {boolean} */ visitParents: function(fn, includeSelf) { // Visit parent nodes (bottom up) if(includeSelf && fn(this) === false){ return false; @@ -1411,14 +1416,16 @@ /* ***************************************************************************** * Fancytree */ /** - * Construct a new tree. - * @class The controller behind a fancytree. - * @name Fancytree - * @constructor + * Construct a new tree object. + * + * @class Fancytree + * @classdesc A Fancytree is 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 @@ -1426,19 +1433,18 @@ * @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 {string} _id + * @property {string} statusClassPropName + * @property {string} ariaPropName + * @property {string} nodeContainerAttrName + * @property {string} $container * @property {FancytreeNode} lastSelectedNode */ -function Fancytree(widget){ - // TODO: rename widget to widget (it's not a jQuery object) +function Fancytree(widget) { this.widget = widget; this.$div = widget.element; this.options = widget.options; this.ext = {}; // Active extension instances this.data = {}; @@ -1461,11 +1467,12 @@ var fakeParent = { tree: this }, $ul; this.rootNode = new FancytreeNode(fakeParent, { title: "root", key: "root_" + this._id, - children: null + children: null, + expanded: true }); this.rootNode.parent = null; // Create root markup $ul = $("<ul>", { @@ -1486,12 +1493,12 @@ .attr("aria-multiselectable", true); } } -Fancytree.prototype = /**@lends Fancytree*/{ - /** Return a context object that can be re-used for _callHook(). +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} */ @@ -1516,15 +1523,15 @@ if(extra){ $.extend(ctx, extra); } return ctx; }, - /** Trigger a hook function: funcName(ctx, [...]). + /* Trigger a hook function: funcName(ctx, [...]). * - * @param {String} funcName + * @param {string} funcName * @param {Fancytree|FancytreeNode|EventData} contextObject - * @param {any, ...} [_extraArgs] optional additional arguments + * @param {any} [_extraArgs] optional additional arguments * @returns {any} */ _callHook: function(funcName, contextObject, _extraArgs) { var ctx = this._makeHookContext(contextObject), fn = this[funcName], @@ -1534,15 +1541,51 @@ } 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. + /* 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 + * @param {string} key * @returns {FancytreeNode} activated node (null, if not found) */ activateKey: function(key) { var node = this.getNodeByKey(key); if(node){ @@ -1550,11 +1593,11 @@ }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 */ @@ -1586,15 +1629,17 @@ if(dd){ dd.cancel(); } }, */ - /** Return the number of child nodes. */ + /** Return the number of nodes. + * @returns {integer} + */ count: function() { return this.rootNode.countChildren(); }, - /** Write to browser console if debugLevel >= 2 (prepending tree info) + /** Write to browser console if debugLevel >= 2 (prepending tree name) * * @param {*} msg string or object or array of such */ debug: function(msg){ if( this.options.debugLevel >= 2 ) { @@ -1609,12 +1654,12 @@ /** * 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] + * @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, @@ -1648,32 +1693,34 @@ checked: true })); } }, /** - * Return node that is active. + * Return the currently active FancytreeNode or null. * @returns {FancytreeNode} */ getActiveNode: function() { return this.activeNode; }, - /** @returns {FancytreeNode | null}*/ + /** 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] + * @param {boolean} [ifTreeHasFocus=false] (not yet implemented) * @returns {FancytreeNode} */ getFocusNode: function(ifTreeHasFocus) { // TODO: implement ifTreeHasFocus return this.focusNode; }, /** - * Return node with a given key. - * @param {String} key + * 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). @@ -1699,12 +1746,12 @@ }, true); return match; }, // TODO: getRoot() /** - * Return a list of selected nodes. - * @param {Boolean} [stopOnParents=false] only return the topmost selected + * 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 = []; @@ -1716,18 +1763,17 @@ } } }); return nodeList; }, - /** - * @returns {Boolean} true if the tree control has keyboard focus + /** 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 info) - * + /** 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()); @@ -1743,73 +1789,26 @@ }, 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') + * 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, @@ -1879,10 +1878,114 @@ __lazyload(key, node, dfd); } // Return a promise that is resovled, when ALL paths were loaded return $.when.apply($, deferredList).promise(); }, + /** 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(); + } + } + }, + /** Reload tree from source and return a promise. + * @param [source] optional new source (defaults to initial source data) + * @returns {$.Promise} + */ + reload: function(source) { + this._callHook("treeClear", this); + return this._callHook("treeLoad", this, source); + }, + /**Render tree (i.e. create DOM elements for all top-level nodes). + * @param {boolean} [force=false] create DOM elemnts, even is parent is collapsed + * @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) { + 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 its children) + * @param {function} [callback(node)] 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 "<Fancytree(#" + this._id + ")>"; + }, + /* _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); + } +}; + +/** + * These additional methods of the {@link Fancytree} class are 'hook functions' + * that can be used and overloaded by extensions. + * (See <a href="https://github.com/mar10/fancytree/wiki/TutorialExtensions">writing extensions</a>.) + * @mixin Fancytree_Hooks + */ +$.extend(Fancytree.prototype, + /** @lends Fancytree_Hooks# */ + { + /** _Default handling for mouse click events. */ nodeClick: function(ctx) { // this.tree.logDebug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which); var activate, expand, event = ctx.originalEvent, @@ -1932,20 +2035,20 @@ if(event.target.localName === "a" && event.target.className === "fancytree-title"){ event.preventDefault(); } // TODO: return promise? }, - nodeCollapseSiblings: function(ctx) { + nodeCollapseSiblings: function(ctx, callOpts) { // TODO: return promise? var ac, i, l, node = ctx.node; if( node.parent ){ ac = node.parent.children; for (i=0, l=ac.length; i<l; i++) { if ( ac[i] !== node && ac[i].expanded ){ - this._callHook("nodeSetExpanded", ac[i], false); + this._callHook("nodeSetExpanded", ac[i], false, callOpts); } } } }, nodeDblclick: function(ctx) { @@ -2164,11 +2267,11 @@ parents[i].setExpanded(true); } }, /** * Remove a single direct child of ctx.node. - * @param ctx + * @param {EventData} ctx * @param {FancytreeNode} childNode dircect child of ctx.node */ nodeRemoveChild: function(ctx, childNode) { var idx, node = ctx.node, @@ -2210,15 +2313,19 @@ var node = ctx.node; FT.debug("nodeRemoveChildMarkup()", node.toString()); // TODO: Unlink attr.ftnode to support GC if(node.ul){ - $(node.ul).remove(); + if( node.isRoot() ) { + $(node.ul).empty(); + } else { + $(node.ul).remove(); + node.ul = null; + } node.visit(function(n){ n.li = n.ul = null; }); - node.ul = null; } }, /**Remove all descendants of ctx.node. * @param {EventData} ctx */ @@ -2294,13 +2401,13 @@ * </ul> * </li> * </code> * * @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 + * @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. * @@ -2308,27 +2415,35 @@ * create markup * - node was rendered: exit fast * - children have been added * - childern have been removed */ - var childLI, childNode1, childNode2, i, l, subCtx, + var childLI, childNode1, childNode2, i, l, next, 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()); + 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"); +// 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); +// } // Render the node if( !isRootNode ){ // Discard markup on force-mode, or if it is not linked to parent <ul> if(node.li && (force || (node.li.parentNode !== node.parent.ul) ) ){ if(node.li.parentNode !== node.parent.ul){ @@ -2358,29 +2473,22 @@ 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 <span> (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); } } @@ -2406,12 +2514,24 @@ // Add child markup for(i=0, l=children.length; i<l; i++) { subCtx = $.extend({}, ctx, {node: children[i]}); this.nodeRender(subCtx, force, deep, false, true); } + // Remove <li> if nodes have moved to another parent + childLI = node.ul.firstChild; + while( childLI ){ + childNode2 = childLI.ftnode; + if( childNode2 && childNode2.parent !== node ) { + node.debug("_fixParent: remove missing " + childNode2, childLI); + next = childLI.nextSibling; + childLI.parentNode.removeChild(childLI); + childLI = next; + }else{ + childLI = childLI.nextSibling; + } + } // Make sure, that <li> order matches node.children order. -// this.nodeFixOrder(ctx); childLI = node.ul.firstChild; for(i=0, l=children.length-1; i<l; i++) { childNode1 = children[i]; childNode2 = childLI.ftnode; if( childNode1 !== childNode2 ) { @@ -2419,11 +2539,10 @@ node.ul.insertBefore(childNode1.li, childNode2.li); } else { childLI = childLI.nextSibling; } } - // TODO: need to check, if node.ul has <li>s, that are not in node.children[] ? } }else{ // No children: remove markup if any if( node.ul ){ // alert("remove child markup for " + node); @@ -2437,18 +2556,17 @@ // Finally add the whole structure to the DOM, so the browser can render if(firstTime){ parent.ul.appendChild(node.li); } } - return; }, /** Create HTML for the node's outer <span> (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, + var id, imageSrc, nodeTitle, role, tabindex, tooltip, node = ctx.node, tree = ctx.tree, opts = ctx.options, aria = opts.aria, level = node.getLevel(), @@ -2511,20 +2629,17 @@ if(!nodeTitle){ // TODO: escape tooltip string tooltip = node.tooltip ? " title='" + node.tooltip.replace(/\"/g, "&quot;") + "'" : ""; id = aria ? " id='ftal_" + node.key + "'" : ""; role = aria ? " role='treeitem'" : ""; -// href = node.data.href || "#"; -// if( opts.nolink || node.nolink ) { -// nodeTitle = "<span role='treeitem' tabindex='-1' class='fancytree-title'" + id + tooltip + ">" + node.title + "</span>"; - nodeTitle = "<span " + role + " class='fancytree-title'" + id + tooltip + ">" + node.title + "</span>"; -// } else { -// nodeTitle = "<a href='" + href + "' tabindex='-1' class='fancytree-title'" + tooltip + ">" + node.title + "</a>"; -// } + tabindex = opts.titlesTabbable ? " tabindex='0'" : ""; + + nodeTitle = "<span " + role + " class='fancytree-title'" + id + tooltip + tabindex + ">" + node.title + "</span>"; } ares.push(nodeTitle); // Note: this will trigger focusout, if node had the focus + //$(node.span).html(ares.join("")); // it will cleanup the jQuery data currently associated with SPAN (if any), but it executes more slowly node.span.innerHTML = ares.join(""); }, /** Update element classes according to node state. * @param {EventData} ctx */ @@ -2637,20 +2752,23 @@ /** 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] + * @param {boolean} [flag=true] + * @param {object} [opts] additional options. Defaults to {} */ - nodeSetActive: function(ctx, flag) { + nodeSetActive: function(ctx, flag, callOpts) { // Handle user click / [space] / [enter], according to clickFolderMode. + callOpts = callOpts || {}; 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){ @@ -2682,26 +2800,28 @@ } }, /** Expand or collapse node, return Deferred.promise. * * @param {EventData} ctx - * @param {Boolean} [flag=true] + * @param {boolean} [flag=true] + * @param {object} [opts] additional options. Defaults to {noAnimation: false} * @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) { + nodeSetExpanded: function(ctx, flag, callOpts) { + callOpts = callOpts || {}; var _afterLoad, dfd, i, l, parents, prevAC, node = ctx.node, tree = ctx.tree, - opts = ctx.options; + opts = ctx.options, + noAnimation = callOpts.noAnimation === true; + // 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() ){ @@ -2712,31 +2832,35 @@ return _getRejectedPromise(node, ["locked"]); }else if ( this._triggerNodeEvent("beforeExpand", node, ctx.originalEvent) === false ){ // Callback returned false return _getRejectedPromise(node, ["rejected"]); } - // + // If this node inside a collpased node, no animation and scrolling is needed + if( !noAnimation && !node.isVisible() ) { + noAnimation = callOpts.noAnimation = true; + } + 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; i<l; i++){ // TODO: should return promise? - this._callHook("nodeCollapseSiblings", parents[i]); + this._callHook("nodeCollapseSiblings", parents[i], callOpts); } }finally{ opts.autoCollapse = prevAC; } } // Trigger expand/collapse after expanding dfd.done(function(){ ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); - if(opts.autoScroll){ + if( opts.autoScroll && !noAnimation ) { // Scroll down to last child, but keep current node visible node.getLastChild().scrollIntoView(true, node); } }); @@ -2763,12 +2887,14 @@ if( node.ul ) { isVisible = (node.ul.style.display !== "none"); isExpanded = !!node.expanded; if ( isVisible === isExpanded ) { node.warn("nodeSetExpanded: UL.style.display already set"); - } else if ( !opts.fx ) { + + } else if ( !opts.fx || noAnimation ) { node.ul.style.display = ( node.expanded || !parent ) ? "" : "none"; + } else { duration = opts.fx.duration || 200; easing = opts.fx.easing; node.debug("nodeSetExpanded: animate start..."); $(node.ul).animate(opts.fx, duration, easing, function(){ @@ -2814,11 +2940,11 @@ node.debug("nodeSetExpanded: returns"); return dfd.promise(); }, /** * @param {EventData} ctx - * @param {Boolean} [flag=true] + * @param {boolean} [flag=true] */ nodeSetFocus: function(ctx, flag) { ctx.node.debug("nodeSetFocus(" + flag + ")"); var ctx2, tree = ctx.tree, @@ -2859,11 +2985,11 @@ } }, /** (De)Select node, return new status (sync). * * @param {EventData} ctx - * @param {Boolean} [flag=true] + * @param {boolean} [flag=true] */ nodeSetSelected: function(ctx, flag) { var node = ctx.node, tree = ctx.tree, opts = ctx.options; @@ -3076,125 +3202,37 @@ 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 "<Fancytree(#" + this._id + ")>"; - }, - /** _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 */ +/** + * This constructor is not called directly. Use `$(selector).fancytre({})` + * to initialize the plugin instead. + * + * @class ui.fancytree + * @classdesc The plugin (derrived from <a href=" http://api.jqueryui.com/jQuery.widget/">jQuery.Widget</a>).<br> + * <pre class="sh_javascript sunlight-highlight-javascript">// Access instance methods and members: + * var tree = $(selector).fancytree("getTree"); + * // Access static members: + * alert($.ui.fancytree.version); + * </pre> + */ $.widget("ui.fancytree", - /** @lends $.ui.fancytree.prototype */ + /** @lends ui.fancytree# */ { /**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 @@ -3223,10 +3261,11 @@ strings: { loading: "Loading&#8230;", loadError: "Load error!" }, tabbable: true, + titlesTabbable: false, _classNames: { node: "fancytree-node", folder: "fancytree-folder", combinedExpanderPrefix: "fancytree-exp-", combinedIconPrefix: "fancytree-ico-", @@ -3437,11 +3476,11 @@ /** @returns {FancytreeNode} the active node or null */ getActiveNode: function() { return this.tree.activeNode; }, /** - * @param {String} key + * @param {string} key * @returns {FancytreeNode} the matching node or null */ getNodeByKey: function(key) { return this.tree.getNodeByKey(key); }, @@ -3456,23 +3495,23 @@ }); // $.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.() + * var node = $.ui.fancytree.getNode(element); */ $.extend($.ui.fancytree, - /** @lends $.ui.fancytree */ + /** @lends ui.fancytree */ { - /** @type {String} */ - version: "2.0.0-5", - /** @type {String} */ + /** @type {string} */ + version: "2.0.0-6", + /** @type {string} */ buildType: "release", /** @type {int} */ debugLevel: 1, // used by $.ui.fancytree.debug() and as default for tree.options.debugLevel _nextId: 1, @@ -3501,11 +3540,11 @@ }, /** 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 + * @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. @@ -3676,15 +3715,16 @@ }); return children; }, /** Add Fancytree extension definition to the list of globally available extensions. * - * @param name - * @param definition + * @param {Object} definition */ - registerExtension: function(name, definition){ - $.ui.fancytree._extensions[name] = definition; + registerExtension: function(definition){ + _assert(definition.name != null, "extensions must have a `name` property."); + _assert(definition.version != null, "extensions must have a `version` property."); + $.ui.fancytree._extensions[definition.name] = definition; }, warn: function(msg){ consoleApply("warn", arguments); } }); @@ -3713,171 +3753,22 @@ } */ }(jQuery, window, document)); /*! - * jquery.fancytree.columnview.js - * - * Render tree like a Mac Finder's column view. - * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) - * - * Copyright (c) 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 - */ - -;(function($, window, document, undefined) { - -"use strict"; - -// prevent duplicate loading -// if ( $.ui.fancytree && $.ui.fancytree.version ) { -// $.ui.fancytree.warn("Fancytree: duplicate include"); -// return; -// } - - -/******************************************************************************* - * Private functions and variables - */ -/* -function _assert(cond, msg){ - msg = msg || ""; - if(!cond){ - $.error("Assertion failed " + msg); - } -} -*/ - -/******************************************************************************* - * Private functions and variables - */ -$.ui.fancytree.registerExtension("columnview", { - version: "0.0.1", - // Default options for this extension. - options: { - }, - // Overide virtual methods for this extension. - // `this` : is this extension object - // `this._base` : the Fancytree instance - // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) - treeInit: function(ctx){ - var $tdFirst, $ul, - tree = ctx.tree, - $table = tree.widget.element; - - tree.tr = $("tbody tr", $table)[0]; - tree.columnCount = $(">td", tree.tr).length; - // Perform default behavior - this._super(ctx); - // Standard Fancytree created a root <ul>. Now move this into first table cell - $ul = $(tree.rootNode.ul); - $tdFirst = $(">td", tree.tr).eq(0); - - $ul.removeClass("fancytree-container"); - $ul.removeAttr("tabindex"); - tree.$container = $table; - $table.addClass("fancytree-container fancytree-ext-columnview"); - $table.attr("tabindex", "0"); - - $tdFirst.empty(); - $ul.detach().appendTo($tdFirst); - - // Force some required options - tree.widget.options.autoCollapse = true; -// tree.widget.options.autoActivate = true; - tree.widget.options.fx = false; - tree.widget.options.clickFolderMode = 1; - - // Make sure that only active path is expanded when a node is activated: - $table.bind("fancytreeactivate", function(e, data){ - var i, tdList, - node = data.node, - tree = data.tree, - level = node.getLevel(); - - tree._callHook("nodeCollapseSiblings", node); - // Clear right neighbours - if(level <= tree.columnCount){ - tdList = $(">td", tree.tr); - for(i=level; i<tree.columnCount; i++){ - tdList.eq(i).empty(); - } - } - // Expand nodes on activate, so we populate the right neighbor cell - if(!node.expanded && (node.children || node.lazy)) { - node.setExpanded(); - } - // Adjust keyboard behaviour: - }).bind("fancytreekeydown", function(e, data){ - var next = null; - switch(e.which){ - case $.ui.keyCode.DOWN: - next = data.node.getNextSibling(); - if( next ){ - next.setFocus(); - } - return false; - case $.ui.keyCode.LEFT: - next = data.node.getParent(); - if( next ){ - next.setFocus(); - } - return false; - case $.ui.keyCode.UP: - next = data.node.getPrevSibling(); - if( next ){ - next.setFocus(); - } - return false; - } - }); - }, - nodeRender: function(ctx, force, deep, collapsed, _recursive) { - // Render standard nested <ul> - <li> hierarchy - this._super(ctx, force, deep, collapsed, _recursive); - // Remove expander and add a trailing triangle instead - var level, $tdChild, $ul, - tree = ctx.tree, - node = ctx.node, - $span = $(node.span); - - $span.find("span.fancytree-expander").remove(); - if(node.hasChildren() !== false && !$span.find("span.fancytree-cv-right").length){ - $span.append($("<span class='fancytree-icon fancytree-cv-right'>")); - } - // Move <ul> with children into the appropriate <td> - if(node.ul){ - node.ul.style.display = ""; // might be hidden if RIGHT was pressed - level = node.getLevel(); - if(level < tree.columnCount){ - $tdChild = $(">td", tree.tr).eq(level); - $ul = $(node.ul).detach(); - $tdChild.empty().append($ul); - } - } - } -}); -}(jQuery, window, document)); - -/*! * jquery.fancytree.dnd.js * - * Drag'N'drop support. + * Drag-and-drop support. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * - * Copyright (c) 2013, Martin Wendt (http://wwWendt.de) + * Copyright (c) 2014, 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ ;(function($, window, document, undefined) { "use strict"; @@ -4066,16 +3957,14 @@ /* ***************************************************************************** * */ -/** @namespace $.ui.fancytree.ext.dnd */ -$.ui.fancytree.registerExtension("dnd", - /** @scope ui_fancytree - * @lends $.ui.fancytree.ext.dnd.prototype - */ + +$.ui.fancytree.registerExtension( { + name: "dnd", version: "0.0.1", // Default options for this extension. options: { // Make tree nodes draggable: dragStart: null, // Callback(sourceNode, data), return true, to enable dnd @@ -4102,11 +3991,11 @@ nodeKeydown: function(ctx) { var event = ctx.originalEvent; if( event.which === $.ui.keyCode.ESCAPE) { this._local._cancelDrag(); } - this._super(ctx); + return this._super(ctx); }, /* Display drop marker according to hitMode ('after', 'before', 'over', 'out', 'start', 'stop'). */ _setDndStatus: function(sourceNode, targetNode, helper, hitMode, accept) { var posOpts, markerOffsetX = 0, @@ -4401,22 +4290,342 @@ } }); }(jQuery, window, document)); /*! + * jquery.fancytree.edit.js + * + * Make node titles editable. + * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) + * + * Copyright (c) 2014, Martin Wendt (http://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.0.0-6 + * @date 2014-02-10T10:52 + */ +/** + * @module fancytree/edit + */ + +;(function($, window, document, undefined) { + +"use strict"; + + +/******************************************************************************* + * Private functions and variables + */ + +var isMac = /Mac/.test(navigator.platform) + // modifiers = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"}, + // specialKeys = { + // 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + // 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + // 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + // 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + // 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + // 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + // 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 186: ";", 191: "/", + // 220: "\\", 222: "'", 224: "meta" + // }, + // shiftNums = { + // "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + // "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + // ".": ">", "/": "?", "\\": "|" + // } + ; + +// $.ui.fancytree.isKeydownEvent = function(e, code){ +// var i, part, partmap, partlist = code.split("+"), len = parts.length; +// var c = String.fromCharCode(e.which).toLowerCase(); +// for( i = 0; i < len; i++ ) { +// } +// alert (parts.unshift()); +// alert (parts.unshift()); +// alert (parts.unshift()); +// }; + + +/** + * [ext-edit] Start inline editing of current node title. + * + * @alias FancytreeNode#editStart + * @requires Fancytree + */ +$.ui.fancytree._FancytreeNodeClass.prototype.editStart = function(){ + var $input, + node = this, + tree = this.tree, + local = tree.ext.edit, + prevTitle = node.title, + instOpts = tree.options.edit, + $title = $(".fancytree-title", node.span), + eventData = {node: node, tree: tree, options: tree.options}; + + if( instOpts.beforeEdit.call(node, {type: "beforeEdit"}, eventData) === false){ + return false; + } + // beforeEdit may want to modify the title before editing + prevTitle = node.title; + + node.debug("editStart"); + // Disable standard Fancytree mouse- and key handling + tree.widget._unbind(); + // #116: ext-dnd prevents the blur event, so we have to catch outer clicks + $(document).on("mousedown.fancytree-edit", function(event){ + if( ! $(event.target).hasClass("fancytree-edit-input") ){ + node.editEnd(true, event); + } + }); + + // Replace node with <input> + $input = $("<input />", { + "class": "fancytree-edit-input", + value: prevTitle + }); + if ( instOpts.adjustWidthOfs != null ) { + $input.width($title.width() + instOpts.adjustWidthOfs); + } + if ( instOpts.inputCss != null ) { + $input.css(instOpts.inputCss); + } + eventData.input = $input; + + $title.html($input); + + $.ui.fancytree.assert(!local.currentNode, "recursive edit"); + local.currentNode = this; + // Focus <input> and bind keyboard handler + $input + .focus() + .change(function(event){ + $input.addClass("fancytree-edit-dirty"); + }).keydown(function(event){ + switch( event.which ) { + case $.ui.keyCode.ESCAPE: + node.editEnd(false, event); + break; + case $.ui.keyCode.ENTER: + node.editEnd(true, event); + return false; // so we don't start editmode on Mac + } + }).blur(function(event){ + return node.editEnd(true, event); + }); + + instOpts.edit.call(node, {type: "edit"}, eventData); +}; + + +/** + * [ext-edit] Stop inline editing. + * @param {Boolean} [applyChanges=false] + * @alias FancytreeNode#editEnd + * @requires jquery.fancytree.edit.js + */ +$.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function(applyChanges, _event){ + var node = this, + tree = this.tree, + local = tree.ext.edit, + instOpts = tree.options.edit, + $title = $(".fancytree-title", node.span), + $input = $title.find("input.fancytree-edit-input"), + newVal = $input.val(), + dirty = $input.hasClass("fancytree-edit-dirty"), + doSave = (applyChanges || (dirty && applyChanges !== false)) && (newVal !== node.title), + eventData = { + node: node, tree: tree, options: tree.options, originalEvent: _event, + dirty: dirty, + save: doSave, + input: $input, + value: newVal + }; + + if( instOpts.beforeClose.call(node, {type: "beforeClose"}, eventData) === false){ + return false; + } + if( doSave && instOpts.save.call(node, {type: "save"}, eventData) === false){ + return false; + } + $input + .removeClass("fancytree-edit-dirty") + .unbind(); + // Unbind outer-click handler + $(document).off(".fancytree-edit"); + + if( doSave ) { + node.setTitle( newVal ); + }else{ + node.renderTitle(); + } + // Re-enable mouse and keyboard handling + tree.widget._bind(); + local.currentNode = null; + node.setFocus(); + // Set keyboard focus, even if setFocus() claims 'nothing to do' + $(tree.$container).focus(); + eventData.input = null; + instOpts.close.call(node, {type: "close"}, eventData); + return true; +}; + + +$.ui.fancytree._FancytreeNodeClass.prototype.startEdit = function(){ + this.warn("FancytreeNode.startEdit() is deprecated. Use .editStart() instead."); + return this.editStart.apply(this, arguments); +}; + + +$.ui.fancytree._FancytreeNodeClass.prototype.endEdit = function(){ + this.warn("FancytreeNode.endEdit() is deprecated. Use .editEnd() instead."); + return this.editEnd.apply(this, arguments); +}; + + +///** +// * Create a new child or sibling node. +// * +// * @param {String} [mode] 'before', 'after', or 'child' +// * @lends FancytreeNode.prototype +// * @requires jquery.fancytree.edit.js +// */ +//$.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function(mode){ +// var newNode, +// node = this, +// tree = this.tree, +// local = tree.ext.edit, +// instOpts = tree.options.edit, +// $title = $(".fancytree-title", node.span), +// $input = $title.find("input.fancytree-edit-input"), +// newVal = $input.val(), +// dirty = $input.hasClass("fancytree-edit-dirty"), +// doSave = (applyChanges || (dirty && applyChanges !== false)) && (newVal !== node.title), +// eventData = { +// node: node, tree: tree, options: tree.options, originalEvent: _event, +// dirty: dirty, +// save: doSave, +// input: $input, +// value: newVal +// }; +// +// node.debug("editCreate"); +// +// if( instOpts.beforeEdit.call(node, {type: "beforeCreateNode"}, eventData) === false){ +// return false; +// } +// newNode = this.addNode({title: "Neuer Knoten"}, mode); +// +// newNode.editStart(); +//}; + + +/** + * [ext-edit] Check if any node in this tree in edit mode. + * + * @returns {FancytreeNode | null} + * @lends Fancytree.prototype + * @requires jquery.fancytree.edit.js + */ +$.ui.fancytree._FancytreeClass.prototype.isEditing = function(){ + return this.ext.edit.currentNode; +}; + + +/** + * [ext-edit] Check if this node is in edit mode. + * @returns {Boolean} true if node is currently beeing edited + * @lends FancytreeNode.prototype + * @requires jquery.fancytree.edit.js + */ +$.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function(){ + return this.tree.ext.edit.currentNode === this; +}; + + +/******************************************************************************* + * Extension code + */ +$.ui.fancytree.registerExtension({ + name: "edit", + version: "0.1.0", + // Default options for this extension. + options: { + adjustWidthOfs: 4, // null: don't adjust input size to content + inputCss: {minWidth: "3em"}, + triggerCancel: ["esc", "tab", "click"], + // triggerStart: ["f2", "dblclick", "shift+click", "mac+enter"], + triggerStart: ["f2", "shift+click", "mac+enter"], + beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available) + beforeEdit: $.noop, // Return false to prevent edit mode + close: $.noop, // Editor was removed + edit: $.noop, // Editor was opened (available as data.input) +// keypress: $.noop, // Not yet implemented + save: $.noop // Save data.input.val() or return false to keep editor open + }, + // Local attributes + currentNode: null, + + // Override virtual methods for this extension. + // `this` : the Fancytree instance + // `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME) + // `this._super`: the virtual function that was overridden (member of previous extension or Fancytree) + treeInit: function(ctx){ + this._super(ctx); + this.$container.addClass("fancytree-ext-edit"); + }, + nodeClick: function(ctx) { + if( $.inArray("shift+click", ctx.options.edit.triggerStart) >= 0 ){ + if( ctx.originalEvent.shiftKey ){ + ctx.node.editStart(); + return false; + } + } + this._super(ctx); + }, + nodeDblclick: function(ctx) { + if( $.inArray("dblclick", ctx.options.edit.triggerStart) >= 0 ){ + ctx.node.editStart(); + return false; + } + return this._super(ctx); + }, + nodeKeydown: function(ctx) { + switch( ctx.originalEvent.which ) { + case 113: // [F2] + if( $.inArray("f2", ctx.options.edit.triggerStart) >= 0 ){ + ctx.node.editStart(); + return false; + } + break; + case $.ui.keyCode.ENTER: + if( $.inArray("mac+enter", ctx.options.edit.triggerStart) >= 0 && isMac ){ + ctx.node.editStart(); + return false; + } + break; + } + return this._super(ctx); + } +}); +}(jQuery, window, document)); + +/*! * jquery.fancytree.filter.js * * Remove or highlight tree nodes, based on a filter. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * - * Copyright (c) 2013, Martin Wendt (http://wwWendt.de) + * Copyright (c) 2014, 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ ;(function($, window, document, undefined) { "use strict"; @@ -4497,11 +4706,12 @@ /******************************************************************************* * Extension code */ -$.ui.fancytree.registerExtension("filter", { +$.ui.fancytree.registerExtension({ + name: "filter", version: "0.0.1", // Default options for this extension. options: { mode: "dimm", leavesOnly: false @@ -4556,161 +4766,147 @@ } }); }(jQuery, window, document)); /*! - * jquery.fancytree.menu.js + * jquery.fancytree.gridnav.js * - * Enable jQuery UI Menu as context menu for tree nodes. + * Support keyboard navigation for trees with embedded input controls. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * - * @see http://api.jqueryui.com/menu/ + * Copyright (c) 2014, Martin Wendt (http://wwWendt.de) * - * Copyright (c) 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ ;(function($, window, document, undefined) { "use strict"; -// prevent duplicate loading -// if ( $.ui.fancytree && $.ui.fancytree.version ) { -// $.ui.fancytree.warn("Fancytree: duplicate include"); -// return; -// } -$.ui.fancytree.registerExtension("menu", { +/******************************************************************************* + * Private functions and variables + */ + +// Allow these navigation keys even when input controls are focused + +var KC = $.ui.keyCode, + // which keys are *not* handled by embedded control, but passed to tree + // navigation handler: + NAV_KEYS = { + "text": [KC.UP, KC.DOWN], + "checkbox": [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], + "radiobutton": [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], + "select-one": [KC.LEFT, KC.RIGHT], + "select-multiple": [KC.LEFT, KC.RIGHT] + }; + + +function findNeighbourTd($target, keyCode){ + var $td = $target.closest("td"); + switch( keyCode ){ + case KC.LEFT: + return $td.prev(); + case KC.RIGHT: + return $td.next(); + case KC.UP: + return $td.parent().prevAll(":visible").first().find("td").eq($td.index()); + case KC.DOWN: + return $td.parent().nextAll(":visible").first().find("td").eq($td.index()); + } + return null; +} + +/******************************************************************************* + * Extension code + */ +$.ui.fancytree.registerExtension({ + name: "gridnav", version: "0.0.1", // Default options for this extension. options: { - enable: true, - selector: null, // - position: {}, // - // Events: - create: $.noop, // - beforeOpen: $.noop, // - open: $.noop, // - focus: $.noop, // - select: $.noop, // - close: $.noop // + autofocusInput: false, // Focus first embedded input if node gets activated + handleCursorKeys: true // Allow UP/DOWN in inputs to move to prev/next node }, - // Override virtual methods for this extension. - // `this` : is this extension object - // `this._base` : the Fancytree instance - // `this._super`: the virtual function that was overridden (member of prev. extension or Fancytree) - treeInit: function(ctx){ - var opts = ctx.options, - tree = ctx.tree; + treeInit: function(ctx){ + // gridnav requires the table extension to be loaded before itself + this._requireExtension("table", true, true); this._super(ctx); - // Prepare an object that will be passed with menu events - tree.ext.menu.data = { - tree: tree, - node: null, - $menu: null, - menuId: null - }; + this.$container.addClass("fancytree-ext-gridnav"); -// tree.$container[0].oncontextmenu = function() {return false;}; - // Replace the standard browser context menu with out own - tree.$container.delegate("span.fancytree-node", "contextmenu", function(event) { - var node = $.ui.fancytree.getNode(event), - ctx = {node: node, tree: node.tree, originalEvent: event, options: tree.options}; - tree.ext.menu._openMenu(ctx); - return false; - }); + // Activate node if embedded input gets focus (due to a click) + this.$container.on("focusin", function(event){ + var ctx2, + node = $.ui.fancytree.getNode(event.target); - // Use jquery.ui.menu - $(opts.menu.selector).menu({ - create: function(event, ui){ - tree.ext.menu.data.$menu = $(this).menu("widget"); - var data = $.extend({}, tree.ext.menu.data); - opts.menu.create.call(tree, event, data); - }, - focus: function(event, ui){ - var data = $.extend({}, tree.ext.menu.data, { - menuItem: ui.item, - menuId: ui.item.find(">a").attr("href") - }); - opts.menu.focus.call(tree, event, data); - }, - select: function(event, ui){ - var data = $.extend({}, tree.ext.menu.data, { - menuItem: ui.item, - menuId: ui.item.find(">a").attr("href") - }); - if( opts.menu.select.call(tree, event, data) !== false){ - tree.ext.menu._closeMenu(ctx); - } + if( node && !node.isActive() ){ + // Call node.setActive(), but also pass the event + ctx2 = ctx.tree._makeHookContext(node, event); + ctx.tree._callHook("nodeSetActive", ctx2, true); } - }).hide(); + }); }, - treeDestroy: function(ctx){ - this._super(ctx); - }, - _openMenu: function(ctx){ - var data, - tree = ctx.tree, - opts = ctx.options, - $menu = $(opts.menu.selector); + nodeSetActive: function(ctx, flag) { + var $outer, + opts = ctx.options.gridnav, + node = ctx.node, + event = ctx.originalEvent || {}, + triggeredByInput = $(event.target).is(":input"); - tree.ext.menu.data.node = ctx.node; - data = $.extend({}, tree.ext.menu.data); + flag = (flag !== false); - if( opts.menu.beforeOpen.call(tree, ctx.originalEvent, data) === false){ - return; - } + this._super(ctx, flag); - $(document).bind("keydown.fancytree", function(event){ - if( event.which === $.ui.keyCode.ESCAPE ){ - tree.ext.menu._closeMenu(ctx); + if( flag ){ + if( ctx.options.titlesTabbable ){ + if( !triggeredByInput ) { + $(node.span).find("span.fancytree-title").focus(); + node.setFocus(); + } + // If one node is tabbable, the container no longer needs to be + ctx.tree.$container.attr("tabindex", "-1"); + // ctx.tree.$container.removeAttr("tabindex"); + } else if( opts.autofocusInput && !triggeredByInput ){ + // Set focus to input sub input (if node was clicked, but not + // when TAB was pressed ) + $outer = $(node.tr || node.span); + $outer.find(":input:enabled:first").focus(); } - }).bind("mousedown.fancytree", function(event){ - // Close menu when clicked outside menu - if( $(event.target).closest(".ui-menu-item").length === 0 ){ - tree.ext.menu._closeMenu(ctx); - } - }); -// $menu.position($.extend({my: "left top", at: "left bottom", of: event}, opts.menu.position)); - $menu - .css("position", "absolute") - .show() - .position({my: "left top", at: "right top", of: ctx.originalEvent, collision: "fit"}) - .focus(); - - opts.menu.open.call(tree, ctx.originalEvent, data); + } }, - _closeMenu: function(ctx){ - var $menu, - tree = ctx.tree, - opts = ctx.options, - data = $.extend({}, tree.ext.menu.data); - if( opts.menu.close.call(tree, ctx.originalEvent, data) === false){ - return; + nodeKeydown: function(ctx) { + var inputType, handleKeys, $td, + opts = ctx.options.gridnav, + event = ctx.originalEvent, + $target = $(event.target); + + // jQuery + inputType = $target.is(":input:enabled") ? $target.prop("type") : null; + ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType); + + if( inputType && opts.handleCursorKeys ){ + handleKeys = NAV_KEYS[inputType]; + if( handleKeys && $.inArray(event.which, handleKeys) >= 0 ){ + $td = findNeighbourTd($target, event.which); + // ctx.node.debug("ignore keydown in input", event.which, handleKeys); + if( $td && $td.length ) { + $td.find(":input:enabled").focus(); + // Prevent Fancytree default navigation + return false; + } + } + return true; } - $menu = $(opts.menu.selector); - $(document).unbind("keydown.fancytree, mousedown.fancytree"); - $menu.hide(); - tree.ext.menu.data.node = null; + ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType); + return this._super(ctx); } -// , -// nodeClick: function(ctx) { -// var event = ctx.originalEvent; -// if(event.which === 2 || (event.which === 1 && event.ctrlKey)){ -// event.preventDefault(); -// ctx.tree.ext.menu._openMenu(ctx); -// return false; -// } -// this._super(ctx); -// } }); }(jQuery, window, document)); /*! * jquery.fancytree.persist.js @@ -4718,17 +4914,17 @@ * Persist tree status in cookiesRemove or highlight tree nodes, based on a filter. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * * @depends: jquery.cookie.js * - * Copyright (c) 2013, Martin Wendt (http://wwWendt.de) + * Copyright (c) 2014, 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ ;(function($, window, document, undefined) { "use strict"; @@ -4801,11 +4997,12 @@ /* ***************************************************************************** * Extension code */ -$.ui.fancytree.registerExtension("persist", { +$.ui.fancytree.registerExtension({ + name: "persist", version: "0.0.1", // Default options for this extension. options: { // appendRequestInfo: false, cookieDelimiter: "~", @@ -4925,31 +5122,33 @@ this._super(ctx); }, // treeDestroy: function(ctx){ // this._super(ctx); // }, - nodeSetActive: function(ctx, flag) { + nodeSetActive: function(ctx, flag, opts) { var instData = this._local, instOpts = this.options.persist; - this._super(ctx, flag); + this._super(ctx, flag, opts); if(instData.storeActive){ $.cookie(instData.cookiePrefix + ACTIVE, this.activeNode ? this.activeNode.key : null, instOpts.cookie); } }, - nodeSetExpanded: function(ctx, flag) { - var node = ctx.node, + nodeSetExpanded: function(ctx, flag, opts) { + var res, + node = ctx.node, instData = this._local; - this._super(ctx, flag); + res = this._super(ctx, flag, opts); if(instData.storeExpanded){ instData._setKey(EXPANDED, node.key, flag); } + return res; }, nodeSetFocus: function(ctx) { var instData = this._local, instOpts = this.options.persist; @@ -4978,17 +5177,17 @@ * jquery.fancytree.table.js * * Render tree as table (aka 'treegrid', 'tabletree'). * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * - * Copyright (c) 2013, Martin Wendt (http://wwWendt.de) + * Copyright (c) 2014, 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ ;(function($, window, document, undefined) { "use strict"; @@ -5045,12 +5244,13 @@ } return prev; } -$.ui.fancytree.registerExtension("table", { - version: "0.0.1", +$.ui.fancytree.registerExtension({ + name: "table", + version: "0.1.0", // Default options for this extension. options: { indentation: 16, // indent every node level by 16px nodeColumnIdx: 0, // render node expander, icon, and title to column #0 checkboxColumnIdx: null // render the checkboxes into the 1st column @@ -5130,20 +5330,20 @@ var children, firstTr, i, l, newRow, prevNode, prevTr, subCtx, tree = ctx.tree, node = ctx.node, opts = ctx.options, isRootNode = !node.parent; -// firstTime = false; + if( !_recursive ){ ctx.hasCollapsedParents = node.parent && !node.parent.expanded; } + $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode); if( !isRootNode ){ if(!node.tr){ // Create new <tr> after previous row newRow = tree.rowFragment.firstChild.cloneNode(true); prevNode = findPrevRowNode(node); -// firstTime = true; // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key); _assert(prevNode); if(collapsed === true && _recursive){ // hide all child rows, so we can use an animation to show it later newRow.style.display = "none"; @@ -5176,11 +5376,11 @@ if ( opts.createNode ){ opts.createNode.call(tree, {type: "createNode"}, ctx); } } else { // Set icon, link, and title (normally this is only required on initial render) - this.nodeRenderTitle(ctx); + //this.nodeRenderTitle(ctx); } } // Allow tweaking after node state was rendered // tree._triggerNodeEvent("renderNode", ctx); if ( opts.renderNode ){ @@ -5201,14 +5401,14 @@ prevTr = node.tr || null; firstTr = tree.tbody.firstChild; // Iterate over all descendants node.visit(function(n){ if(n.tr){ - if(!node.expanded && !isRootNode && n.tr.style.display !== "none"){ - // fix after a node was dropped over a sibling. - // In this case it must be hidden + if(!n.parent.expanded && n.tr.style.display !== "none"){ + // fix after a node was dropped over a collapsed n.tr.style.display = "none"; + setChildRowVisibility(n, false); } if(n.tr.previousSibling !== prevTr){ node.debug("_fixOrder: mismatch at node: " + n); var nextTr = prevTr ? prevTr.nextSibling : firstTr; tree.tbody.insertBefore(n.tr, nextTr); @@ -5219,29 +5419,23 @@ } // Update element classes according to node state if(!isRootNode){ this.nodeRenderStatus(ctx); } - // Finally add the whole structure to the DOM, so the browser can render - // if(firstTime){ - // parent.ul.appendChild(node.li); - // } - // TODO: just for debugging - // this._super(ctx); }, nodeRenderTitle: function(ctx, title) { var $cb, node = ctx.node, opts = ctx.options; this._super(ctx); - // move checkbox to custom column + // Move checkbox to custom column if(opts.checkbox && opts.table.checkboxColumnIdx != null){ $cb = $("span.fancytree-checkbox", node.span).detach(); $(node.tr).find("td:first").html($cb); } - // let user code write column content + // Let user code write column content // ctx.tree._triggerNodeEvent("renderColumns", node); if ( opts.renderColumns ){ opts.renderColumns.call(ctx.tree, {type: "renderColumns"}, ctx); } }, @@ -5249,20 +5443,19 @@ var indent, node = ctx.node, opts = ctx.options; this._super(ctx); + $(node.tr).removeClass("fancytree-node"); // indent indent = (node.getLevel() - 1) * opts.table.indentation; - if(indent){ - $(node.span).css({marginLeft: indent + "px"}); - } + $(node.span).css({marginLeft: indent + "px"}); }, /* Expand node, return Deferred.promise. */ - nodeSetExpanded: function(ctx, flag) { - return this._super(ctx, flag).always(function () { + nodeSetExpanded: function(ctx, flag, opts) { + return this._super(ctx, flag, opts).always(function () { flag = (flag !== false); setChildRowVisibility(ctx.node, flag); }); }, nodeSetStatus: function(ctx, status, message, details) { @@ -5289,26 +5482,27 @@ * Enable jQuery UI ThemeRoller styles. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * * @see http://jqueryui.com/themeroller/ * - * Copyright (c) 2013, Martin Wendt (http://wwWendt.de) + * Copyright (c) 2014, 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 + * @version 2.0.0-6 + * @date 2014-02-10T10:52 */ ;(function($, window, document, undefined) { "use strict"; /******************************************************************************* * Extension code */ -$.ui.fancytree.registerExtension("themeroller", { +$.ui.fancytree.registerExtension({ + name: "themeroller", version: "0.0.1", // Default options for this extension. options: { activeClass: "ui-state-active", foccusClass: "ui-state-focus",