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

- old
+ new

@@ -5,12 +5,12 @@ * * 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-6 - * @date 2014-02-10T10:52 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ /** Core Fancytree module. */ @@ -166,25 +166,40 @@ deferred.resolveWith(context); }; } +function _getElementDataAsDict($el){ + // Evaluate 'data-NAME' attributes with special treatment for 'data-json'. + var d = $.extend({}, $el.data()), + json = d.json; + delete d.fancytree; // added to container by widget factory + if( json ) { + delete d.json; + // <li data-json='...'> is already returned as object (http://api.jquery.com/data/#data-html5) + d = $.extend(d, json); + } + return d; +} + + // TODO: use currying function _makeNodeTitleMatcher(s){ s = s.toLowerCase(); return function(node){ return node.title.toLowerCase().indexOf(s) >= 0; }; } var i, FT = null, // initialized below + ENTITY_MAP = {"&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;", "/": "&#x2F;"}, //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_ATTRS = "active expanded focus folder hideCheckbox lazy selected unselectable".split(" "), CLASS_ATTR_MAP = {}, // Top-level Fancytree node attributes, that can be set by dict - NODE_ATTRS = "expanded extraClasses folder hideCheckbox key lazy selected title tooltip unselectable".split(" "), + NODE_ATTRS = "expanded extraClasses folder hideCheckbox key lazy refKey selected title tooltip unselectable".split(" "), NODE_ATTR_MAP = {}, // Attribute names that should NOT be added to node.data NONE_NODE_DATA_MAP = {"active": true, "children": true, "data": true, "focus": true}; for(i=0; i<CLASS_ATTRS.length; i++){ CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; } @@ -203,34 +218,37 @@ * @classdesc A FancytreeNode represents the hierarchical data model and operations. * * @param {FancytreeNode} parent * @param {NodeData} obj * - * @property {Fancytree} tree - * @property {FancytreeNode} parent Parent node - * @property {string} key - * @property {string} title + * @property {Fancytree} tree The tree instance + * @property {FancytreeNode} parent The parent node + * @property {string} key Node id (must be unique inside the tree) + * @property {string} title Display name (may contain HTML) * @property {object} data Contains all extra data that was passed on node creation - * @property {FancytreeNode[] | null | undefined} children list of child nodes - * @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 + * @property {FancytreeNode[] | null | undefined} children Array of child nodes.<br> + * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array + * to define a node that has no children. + * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property. + * @property {string} extraClasses Addtional CSS classes, added to the node's `&lt;span>` + * @property {boolean} folder Folder nodes have different default icons and click behavior.<br> + * Note: Also non-folders may have children. + * @property {string} statusNodeType null or type of temporarily generated system node like 'loading', or 'error'. + * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion. + * @property {boolean} selected Use isSelected(), setSelected() to access this property. + * @property {string} tooltip Alternative description used as hover banner */ function FancytreeNode(parent, obj){ var i, l, name, cl; this.parent = parent; this.tree = parent.tree; this.ul = null; this.li = null; // <li id='key' ftnode=this> tag - this.isStatusNode = false; + this.statusNodeType = null; // if this is a temp. node to display the status of its parent + this._isLoading = false; // if this node itself is loading + this._error = null; // {message: '...'} if a load error occured this.data = {}; // TODO: merge this code with node.toDict() // copy attributes from obj object for(i=0, l=NODE_ATTRS.length; i<l; i++){ @@ -249,26 +267,39 @@ } } // Fix missing key if( this.key == null ){ // test for null OR undefined - this.key = "_" + (FT._nextNodeKey++); + if( this.tree.options.defaultKey ) { + this.key = this.tree.options.defaultKey(this); + _assert(this.key, "defaultKey() must return a unique key"); + } else { + this.key = "_" + (FT._nextNodeKey++); + } } + // Fix tree.activeNode // TODO: not elegant: we use obj.active as marker to set tree.activeNode // when loading from a dictionary. if(obj.active){ _assert(this.tree.activeNode === null, "only one active node allowed"); this.tree.activeNode = this; } + if( obj.selected ){ // #186 + this.tree.lastSelectedNode = this; + } // TODO: handle obj.focus = true // Create child nodes this.children = null; cl = obj.children; if(cl && cl.length){ this._setChildren(cl); } + // Add to key/ref map (except for root node) +// if( parent ) { + this.tree._callHook("treeRegisterNode", this.tree, true, this); +// } } FancytreeNode.prototype = /** @lends FancytreeNode# */{ /* Return the direct child FancytreeNode with a given key, index. */ @@ -367,10 +398,21 @@ return this.addChildren(node); } _assert(false, "Invalid mode: " + mode); }, /** + * Append new node after this. + * + * This a convenience function that calls addNode(node, 'after') + * + * @param {NodeData} node node definition + * @returns {FancytreeNode} new node + */ + appendSibling: function(node){ + return this.addNode(node, "after"); + }, + /** * Modify existing child nodes. * * @param {NodePatch} patch * @returns {$.Promise} * @see FancytreeNode#addChildren @@ -415,11 +457,11 @@ }else{ promise = _getResolvedPromise(this); } return promise; }, - /** + /** Collapse all sibling nodes. * @returns {$.Promise} */ collapseSiblings: function() { return this.tree._callHook("nodeCollapseSiblings", this); }, @@ -460,16 +502,16 @@ 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.*/ + /** Deprecated. + * @deprecated since 2014-02-16. Use resetLazy() instead. + */ discard: function(){ - if(this.lazy && $.isArray(this.children)){ - this.removeChildren(); - return this.setExpanded(false); - } + this.warn("FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead."); + return this.resetLazy(); }, // TODO: expand(flag) /**Find all nodes that contain `match` in the title. * * @param {string | function(node)} match string to search for, of a function that @@ -527,11 +569,11 @@ this.partsel = true; break; default: _assert(false, "invalid state: " + state); } - this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); + // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); if( changed ){ this.renderStatus(); } return changed; }, @@ -656,38 +698,46 @@ this.data = $.extend(this.data, dict); this.removeChildren(); this.addChild(children); */ }, - /** @returns {FancytreeNode[] | undefined} list of child nodes (undefined for unexpanded lazy nodes).*/ + /** Return the list of child nodes (undefined for unexpanded lazy nodes). + * @returns {FancytreeNode[] | undefined} + */ getChildren: function() { if(this.hasChildren() === undefined){ // TODO: only required for lazy nodes? return undefined; // Lazy node: unloaded, currently loading, or load error } return this.children; }, - /** @returns {FancytreeNode | null}*/ + /** Return the first child node or null. + * @returns {FancytreeNode | null} + */ getFirstChild: function() { return this.children ? this.children[0] : null; }, - /** @returns {int} 0-based child index.*/ + /** Return the 0-based child index. + * @returns {int} + */ getIndex: function() { // return this.parent.children.indexOf(this); return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7 }, - /**@returns {string} hierarchical child index (1-based: '3.2.4').*/ + /** Return the hierarchical child index (1-based, e.g. '3.2.4'). + * @returns {string} + */ getIndexHier: function(separator) { separator = separator || "."; var res = []; $.each(this.getParentList(false, true), function(i, o){ res.push(o.getIndex() + 1); }); return res.join(separator); }, - /** + /** Return the parent keys separated by options.keyPathSeparator, e.g. "id_1/id_17/id_32". * @param {boolean} [excludeSelf=false] - * @returns {string} parent keys separated by options.keyPathSeparator + * @returns {string} */ getKeyPath: function(excludeSelf) { var path = [], sep = this.tree.options.keyPathSeparator; this.visitParents(function(n){ @@ -695,25 +745,31 @@ path.unshift(n.key); } }, !excludeSelf); return sep + path.join(sep); }, - /**@returns {FancytreeNode | null} last child of this node.*/ + /** Return the last child of this node or null. + * @returns {FancytreeNode | null} + */ getLastChild: function() { return this.children ? this.children[this.children.length - 1] : null; }, - /** @returns {int} node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, .... */ + /** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... . + * @returns {int} + */ getLevel: function() { var level = 0, dtn = this.parent; while( dtn ) { level++; dtn = dtn.parent; } return level; }, - /** @returns {FancytreeNode | null} */ + /** Return the successor node (under the same parent) or null. + * @returns {FancytreeNode | null} + */ getNextSibling: function() { // TODO: use indexOf, if available: (not in IE6) if( this.parent ){ var i, l, ac = this.parent.children; @@ -724,18 +780,20 @@ } } } return null; }, - /** @returns {FancytreeNode | null} returns null for the system root node*/ + /** Return the parent node (null for the system root node). + * @returns {FancytreeNode | null} + */ getParent: function() { // TODO: return null for top-level nodes? return this.parent; }, - /** - * @param {boolean} [includeRoot=false] - * @param {boolean} [includeSelf=false] + /** Return an array of all parent nodes (top-down). + * @param {boolean} [includeRoot=false] Include the invisible system root node. + * @param {boolean} [includeSelf=false] Include the node itself. * @returns {FancytreeNode[]} */ getParentList: function(includeRoot, includeSelf) { var l = [], dtn = includeSelf ? this : this.parent; @@ -745,11 +803,13 @@ } dtn = dtn.parent; } return l; }, - /** @returns {FancytreeNode | null} */ + /** Return the predecessor node (under the same parent) or null. + * @returns {FancytreeNode | null} + */ getPrevSibling: function() { if( this.parent ){ var i, l, ac = this.parent.children; @@ -759,45 +819,51 @@ } } } return null; }, - /** @returns {boolean | undefined} Check if node has children (returns undefined, if not sure). */ + /** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded). + * @returns {boolean | undefined} + */ hasChildren: function() { if(this.lazy){ if(this.children == null ){ // null or undefined: Not yet loaded return undefined; }else if(this.children.length === 0){ // Loaded, but response was empty return false; - }else if(this.children.length === 1 && this.children[0].isStatusNode ){ + }else if(this.children.length === 1 && this.children[0].isStatusNode() ){ // Currently loading or load error return undefined; } return true; } return !!this.children; }, - /**@returns {boolean} true, if node has keyboard focus*/ + /** Return true if node has keyboard focus. + * @returns {boolean} + */ hasFocus: function() { return (this.tree.hasFocus() && this.tree.focusNode === this); }, - /**@returns {boolean} true, if node is active*/ + /** Return true if node is active (see also FancytreeNode#isSelected). + * @returns {boolean} + */ isActive: function() { return (this.tree.activeNode === this); }, - /** + /** Return true if node is a direct child of otherNode. * @param {FancytreeNode} otherNode - * @returns {boolean} true, if node is a direct child of otherNode + * @returns {boolean} */ isChildOf: function(otherNode) { return (this.parent && this.parent === otherNode); }, - /** + /** Return true, if node is a direct or indirect sub node of otherNode. * @param {FancytreeNode} otherNode - * @returns {boolean} true, if node is a sub node of otherNode + * @returns {boolean} */ isDescendantOf: function(otherNode) { if(!otherNode || otherNode.tree !== this.tree){ return false; } @@ -808,80 +874,173 @@ } p = p.parent; } return false; }, - /** @returns {boolean} true, if node is expanded*/ + /** Return true if node is expanded. + * @returns {boolean} + */ isExpanded: function() { return !!this.expanded; }, - /** @returns {boolean}*/ + /** Return true if node is the first node of its parent's children. + * @returns {boolean} + */ isFirstSibling: function() { var p = this.parent; return !p || p.children[0] === this; }, - /** @returns {boolean}*/ + /** Return true if node is a folder, i.e. has the node.folder attribute set. + * @returns {boolean} + */ isFolder: function() { return !!this.folder; }, - /** @returns {boolean}*/ + /** Return true if node is the last node of its parent's children. + * @returns {boolean} + */ isLastSibling: function() { var p = this.parent; return !p || p.children[p.children.length-1] === this; }, - /** @returns {boolean} true, if node is lazy (even if data was already loaded)*/ + /** Return true if node is lazy (even if data was already loaded) + * @returns {boolean} + */ isLazy: function() { return !!this.lazy; }, - /** @returns {boolean} true, if children are currently beeing loaded*/ + /** Return true if node is lazy and loaded. For non-lazy nodes always return true. + * @returns {boolean} + */ + isLoaded: function() { + return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node + }, + /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending. + * @returns {boolean} + */ isLoading: function() { - _raiseNotImplemented(); // TODO: implement + return !!this._isLoading; }, - /**@returns {boolean} true, if node is the (invisible) system root node*/ + /** Return true if this is the (invisible) system root node. + * @returns {boolean} + */ isRoot: function() { return (this.tree.rootNode === this); }, - /** @returns {boolean} true, if node is selected (e.g. has a checkmark set)*/ + /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive). + * @returns {boolean} + */ isSelected: function() { return !!this.selected; }, - // TODO: use _isStatusNode as class attribute name -// isStatusNode: function() { -// return (this.data.isStatusNode === true); -// }, - /** Return true, if all parents are expanded. */ + /** Return true if this node is a temporarily generated system node like + * 'loading', or 'error' (node.statusNodeType contains the type). + * @returns {boolean} + */ + isStatusNode: function() { + return !!this.statusNodeType; + }, + /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false. + * @returns {boolean} + */ + isUndefined: function() { + return this.hasChildren() === undefined; // also checks if the only child is a status node + }, + /** Return true if all parent nodes are expanded. Note: this does not check + * whether the node is scrolled into the visible part of the screen. + * @returns {boolean} + */ isVisible: function() { var i, l, parents = this.getParentList(false, false); for(i=0, l=parents.length; i<l; i++){ if( ! parents[i].expanded ){ return false; } } return true; }, - /** Expand all parents and optionally scroll into visible area as neccessary (async). - * + /** Deprecated. + * @deprecated since 2014-02-16: use load() instead. */ - makeVisible: function() { - // TODO: implement scolling (http://www.w3.org/TR/wai-aria-practices/#visualfocus) - // TODO: return $.promise - var i, l, - parents = this.getParentList(false, false); + lazyLoad: function(discard) { + this.warn("FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead."); + return this.load(discard); + }, + /** + * Load all children of a lazy node. + * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. + * @returns {$.Promise} + */ + load: function(forceReload) { + var res, source, + that = this; - for(i=0, l=parents.length; i<l; i++){ - parents[i].setExpanded(true); + _assert( this.isLazy(), "load() requires a lazy node" ); + _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" ); + + if( this.isLoaded() ){ + this.resetLazy(); // also collapses } + // This method is also called by setExpanded() and loadKeyPath(), so we + // have to avoid recursion. + source = this.tree._triggerNodeEvent("lazyLoad", this); + if( source === false ) { // #69 + return _getResolvedPromise(this); + } + _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result"); + res = this.tree._callHook("nodeLoadChildren", this, source); + if( this.expanded ) { + res.always(function(){ + that.render(); + }); + } + return res; }, + /** Expand all parents and optionally scroll into visible area as neccessary. + * Promise is resolved, when lazy loading and animations are done. + * @param {object} [opts] passed to `setExpanded()`. + * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} + * @returns {$.Promise} + */ + makeVisible: function(opts) { + var i, + that = this, + deferreds = [], + dfd = new $.Deferred(), + parents = this.getParentList(false, false), + len = parents.length, + effects = !(opts && opts.noAnimation === true), + scroll = !(opts && opts.scrollIntoView === false); + + // Expand bottom-up, so only the top node is animated + for(i = len - 1; i >= 0; i--){ + // that.debug("pushexpand" + parents[i]); + deferreds.push(parents[i].setExpanded(true, opts)); + } + $.when.apply($, deferreds).done(function(){ + // All expands have finished + // that.debug("expand DONE", scroll); + if( scroll ){ + that.scrollIntoView(effects).done(function(){ + // that.debug("scroll DONE"); + dfd.resolve(); + }); + } else { + dfd.resolve(); + } + }); + return dfd.promise(); + }, /** Move this node to targetNode. * @param {FancytreeNode} targetNode - * @param {string} mode + * @param {string} mode <pre> * 'child': append this node as last child of targetNode. * This is the default. To be compatble with the D'n'd * hitMode, we also accept 'over'. * 'before': add this node as sibling before targetNode. - * 'after': add this node as sibling after targetNode. - * @param [map] optional callback(FancytreeNode) to allow modifcations + * 'after': add this node as sibling after targetNode.</pre> + * @param {function} [map] optional callback(FancytreeNode) to allow modifcations */ moveTo: function(targetNode, mode, map) { if(mode === undefined || mode === "over"){ mode = "child"; } @@ -991,12 +1150,12 @@ // 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 ) + // DT issue #82: only if not initializing, because the children may not exist yet +// if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing ) // ftnode._fixSelectionState(); // In multi-hier mode, update the parents selection state if( ftnode.bSelected && opts.selectMode==3 ) { var p = this; @@ -1089,56 +1248,78 @@ default: handled = false; } }, /** - * Discard and reload all children of a lazy node. - * @param {boolean} [discard=false] - * @returns $.Promise + * Remove this node (not allowed for system root). */ - 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); + remove: function() { + return this.parent.removeChild(this); }, /** - * @see Fancytree_Hooks#nodeRender + * Remove childNode from list of direct children. + * @param {FancytreeNode} childNode */ + removeChild: function(childNode) { + return this.tree._callHook("nodeRemoveChild", this, childNode); + }, + /** + * Remove all child nodes and descendents. This converts the node into a leaf.<br> + * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy() + * in order to trigger lazyLoad on next expand. + */ + removeChildren: function() { + return this.tree._callHook("nodeRemoveChildren", this); + }, + /** + * This method renders and updates all HTML markup that is required + * to display this node in its current state.<br> + * Note: + * <ul> + * <li>It should only be neccessary to call this method after the node object + * was modified by direct access to its properties, because the common + * API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...) + * already handle this. + * <li> {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} + * are implied. If changes are more local, calling only renderTitle() or + * renderStatus() may be sufficient and faster. + * <li>If a node was created/removed, node.render() must be called <i>on the parent</i>. + * </ul> + * + * @param {boolean} [force=false] re-render, even if html markup was already created + * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed + */ render: function(force, deep) { return this.tree._callHook("nodeRender", this, force, deep); }, - /** + /** Create HTML markup for the node's outer <span> (expander, checkbox, icon, and title). * @see Fancytree_Hooks#nodeRenderTitle */ renderTitle: function() { return this.tree._callHook("nodeRenderTitle", this); }, - /** + /** Update element's CSS classes according to node state. * @see Fancytree_Hooks#nodeRenderStatus */ renderStatus: function() { return this.tree._callHook("nodeRenderStatus", this); }, - /** Remove this node (not allowed for root).*/ - remove: function() { - return this.parent.removeChild(this); + /** + * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad + * event is triggered on next expand. + */ + resetLazy: function() { + this.removeChildren(); + this.expanded = false; + this.lazy = true; + this.children = undefined; + this.renderStatus(); }, - /**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). + * @param {string} mode + * @param {number} ms */ scheduleAction: function(mode, ms) { if( this.tree.timer ) { clearTimeout(this.tree.timer); // this.tree.debug("clearTimeout(%o)", this.tree.timer); @@ -1169,16 +1350,17 @@ /** * * @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 + * @returns {$.Promise} */ scrollIntoView: function(effects, topNode) { effects = (effects === true) ? {duration: 200, queue: false} : effects; var topNodeY, dfd = new $.Deferred(), + that = this, 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)), @@ -1208,11 +1390,16 @@ } if(newScrollTop !== null){ if(effects){ // TODO: resolve dfd after animation // var that = this; - $container.animate({scrollTop: newScrollTop}, effects); + effects.complete = function(){ + dfd.resolveWith(that); + }; + $container.animate({ + scrollTop: newScrollTop + }, effects); }else{ $container[0].scrollTop = newScrollTop; dfd.resolveWith(this); } }else{ @@ -1243,14 +1430,14 @@ * @param {object} [opts] additional options. Defaults to {noEvents: false} */ setActive: function(flag, opts){ return this.tree._callHook("nodeSetActive", this, flag, opts); }, - /**Expand or collapse this node. + /**Expand or collapse this node. Promise is resolved, when lazy loading and animations are done. * @param {boolean} [flag=true] pass false to collapse * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false} - * @returns {$.Promise} resolved, when lazy loading and animations are done + * @returns {$.Promise} */ setExpanded: function(flag, opts){ return this.tree._callHook("nodeSetExpanded", this, flag, opts); }, /**Set keyboard focus to this node. @@ -1259,11 +1446,11 @@ */ setFocus: function(flag){ return this.tree._callHook("nodeSetFocus", this, flag); }, // TODO: setLazyNodeStatus - /**Select this node. + /**Select this node, i.e. check the checkbox. * @param {boolean} [flag=true] pass false to deselect */ setSelected: function(flag){ return this.tree._callHook("nodeSetSelected", this, flag); }, @@ -1332,11 +1519,11 @@ if( recursive ) { if(this.hasChildren()){ dict.children = []; for(i=0, l=this.children.length; i<l; i++ ){ node = this.children[i]; - if( !node.isStatusNode ){ + if( !node.isStatusNode() ){ dict.children.push(node.toDict(true, callback)); } } }else{ // dict.children = null; @@ -1353,16 +1540,18 @@ return this.tree._callHook("nodeToggleSelected", this); }, toString: function() { return "<FancytreeNode(#" + this.key + ", '" + this.title + "')>"; }, - /** Call fn(node) for all child nodes. Stop iteration, if fn() returns false. - * Skip current branch, if fn() returns 'skip'. + /** Call fn(node) for all child nodes.<br> + * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br> + * Return false if iteration was stopped. + * * @param {function} fn the callback function. * Return false to stop iteration, return "skip" to skip this node and children only. * @param {boolean} [includeSelf=false] - * @returns {boolean} false, if the iterator was stopped. + * @returns {boolean} */ visit: function(fn, includeSelf) { var i, l, res = true, children = this.children; @@ -1381,14 +1570,17 @@ } } } return res; }, - /** + /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br> + * Stop iteration, if fn() returns false.<br> + * Return false if iteration was stopped. * - * @param fn - * @param includeSelf + * @param {function} fn the callback function. + * Return false to stop iteration, return "skip" to skip this node and children only. + * @param {boolean} [includeSelf=false] * @returns {boolean} */ visitParents: function(fn, includeSelf) { // Visit parent nodes (bottom up) if(includeSelf && fn(this) === false){ @@ -1419,11 +1611,11 @@ */ /** * Construct a new tree object. * * @class Fancytree - * @classdesc A Fancytree is the controller behind a fancytree. + * @classdesc The controller behind a fancytree. * This class also contains 'hook methods': see {@link Fancytree_Hooks}. * * @param {Widget} widget * * @property {FancytreeOptions} options @@ -1444,19 +1636,28 @@ */ function Fancytree(widget) { this.widget = widget; this.$div = widget.element; this.options = widget.options; + if( this.options && $.isFunction(this.options.lazyload) ) { + if( ! $.isFunction(this.options.lazyLoad ) ) { + this.options.lazyLoad = function() { + FT.warn("The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead."); + widget.options.lazyload.apply(this, arguments); + }; + } + } this.ext = {}; // Active extension instances - this.data = {}; + // allow to init tree.data.foo from <div data-foo=''> + this.data = _getElementDataAsDict(this.$div); this._id = $.ui.fancytree._nextId++; this._ns = ".fancytree-" + this._id; // append for namespaced events this.activeNode = null; this.focusNode = null; this._hasFocus = null; this.lastSelectedNode = null; - this.systemFocusElement = null, + this.systemFocusElement = null; this.statusClassPropName = "span"; this.ariaPropName = "li"; this.nodeContainerAttrName = "li"; @@ -1693,11 +1894,11 @@ checked: true })); } }, /** - * Return the currently active FancytreeNode or null. + * Return the currently active node or null. * @returns {FancytreeNode} */ getActiveNode: function() { return this.activeNode; }, @@ -1860,17 +2061,17 @@ // Now load all lazy nodes and continue itearation for remaining paths deferredList = []; // Avoid jshint warning 'Don't make functions within a loop.': function __lazyload(key, node, dfd){ callback.call(self, node, "loading"); - node.lazyLoad().done(function(){ - self.loadKeyPath.call(self, loadMap[key], callback, node).always(_makeResolveFunc(dfd, self)); - }).fail(function(errMsg){ - self.warn("loadKeyPath: error loading: " + key + " (parent: " + root + ")"); - callback.call(self, node, "error"); - dfd.reject(); - }); + node.load().done(function(){ + self.loadKeyPath.call(self, loadMap[key], callback, node).always(_makeResolveFunc(dfd, self)); + }).fail(function(errMsg){ + self.warn("loadKeyPath: error loading: " + key + " (parent: " + root + ")"); + callback.call(self, node, "error"); + dfd.reject(); + }); } for(key in loadMap){ node = root._findDirectChild(key); // alert("loadKeyPath: lazy node(" + key + ") = " + node); dfd = new $.Deferred(); @@ -1981,12 +2182,14 @@ * @mixin Fancytree_Hooks */ $.extend(Fancytree.prototype, /** @lends Fancytree_Hooks# */ { - - /** _Default handling for mouse click events. */ + /** Default handling for mouse click events. + * + * @param {EventData} ctx + */ nodeClick: function(ctx) { // this.tree.logDebug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which); var activate, expand, event = ctx.originalEvent, targetType = ctx.targetType, @@ -1995,15 +2198,15 @@ // TODO: use switch // TODO: make sure clicks on embedded <input> 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 +// this._callHook("nodeSetFocus", ctx, true); // DT issue 95 } else if( targetType === "checkbox" ) { // Clicking the checkbox always (de)selects this._callHook("nodeToggleSelected", ctx); - this._callHook("nodeSetFocus", ctx, true); // issue 95 + this._callHook("nodeSetFocus", ctx, true); // DT issue 95 } else { // Honor `clickFolderMode` for expand = false; activate = true; if( node.folder ) { @@ -2035,10 +2238,15 @@ if(event.target.localName === "a" && event.target.className === "fancytree-title"){ event.preventDefault(); } // TODO: return promise? }, + /** Collapse all other children of same parent. + * + * @param {EventData} ctx + * @param {object} callOpts + */ nodeCollapseSiblings: function(ctx, callOpts) { // TODO: return promise? var ac, i, l, node = ctx.node; @@ -2049,10 +2257,13 @@ this._callHook("nodeSetExpanded", ac[i], false, callOpts); } } } }, + /** Default handling for mouse douleclick events. + * @param {EventData} ctx + */ nodeDblclick: function(ctx) { // TODO: return promise? if( ctx.targetType === "title" && ctx.options.clickFolderMode === 4) { // this.nodeSetFocus(ctx); // this._callHook("nodeSetActive", ctx, true); @@ -2064,10 +2275,11 @@ } }, /** Default handling for mouse keydown events. * * NOTE: this may be called with node == null if tree (but no node) has focus. + * @param {EventData} ctx */ nodeKeydown: function(ctx) { // TODO: return promise? var res, event = ctx.originalEvent, @@ -2126,21 +2338,16 @@ // /** Default handling for mouse keypress events. */ // nodeKeypress: function(ctx) { // var event = ctx.originalEvent; // }, - // /** Trigger lazyload event (async). */ + // /** Trigger lazyLoad event (async). */ // nodeLazyLoad: function(ctx) { // var node = ctx.node; // if(this._triggerNodeEvent()) // }, - /** Load children (async). - * source may be - * - an array of children - * - a node object - * - an Ajax options object - * - an Ajax.promise + /** Load child nodes (async). * * @param {EventData} ctx * @param {object[]|object|string|$.Promise|function} source * @returns {$.Promise} The deferred will be resolved as soon as the (ajax) * data was rendered. @@ -2176,13 +2383,16 @@ }else{ source = $.ajax(ajax); } // TODO: change 'pipe' to 'then' for jQuery 1.8 + // $.pipe returns a new Promise with filtered results source = source.pipe(function (data, textStatus, jqXHR) { var res; - if(typeof data === "string"){ $.error("Ajax request returned a string (did you get the JSON dataType wrong?)."); } + if(typeof data === "string"){ + $.error("Ajax request returned a string (did you get the JSON dataType wrong?)."); + } // postProcess is similar to the standard dataFilter hook, // but it is also called for JSONP if( ctx.options.postProcess ){ res = tree._triggerNodeEvent("postProcess", ctx, ctx.originalEvent, {response: data, dataType: this.dataType}); data = $.isArray(res) ? res : data; @@ -2200,12 +2410,15 @@ }); }); } if($.isFunction(source.promise)){ - // `source` is a promise + // `source` is a deferred, i.e. ajax request + _assert(!node.isLoading()); + // node._isLoading = true; tree.nodeSetStatus(ctx, "loading"); + source.done(function () { tree.nodeSetStatus(ctx, "ok"); }).fail(function(error){ var ctxErr; if (error.node && error.error && error.message) { @@ -2220,55 +2433,39 @@ } tree._triggerNodeEvent("loaderror", ctxErr, null); tree.nodeSetStatus(ctx, "error", ctxErr.message, ctxErr.details); }); } - + // $.when(source) resolves also for non-deferreds return $.when(source).done(function(children){ var metaData; if( $.isPlainObject(children) ){ // We got {foo: 'abc', children: [...]} - // Copy extra properties to tree.data. + // Copy extra properties to tree.data.foo _assert($.isArray(children.children), "source must contain (or be) an array of children"); _assert(node.isRoot(), "source may only be an object for root nodes"); metaData = children; children = children.children; delete metaData.children; $.extend(tree.data, metaData); } _assert($.isArray(children), "expected array of children"); node._setChildren(children); - if(node.parent){ - // trigger fancytreeloadchildren (except for tree-reload) - tree._triggerNodeEvent("loadChildren", node); - } + // trigger fancytreeloadchildren + // if( node.parent ) { + tree._triggerNodeEvent("loadChildren", node); + // } + // }).always(function(){ + // node._isLoading = false; }); }, - // isVisible: function() { - // // Return true, if all parents are expanded. - // var parents = ctx.node.getParentList(false, false); - // for(var i=0, l=parents.length; i<l; i++){ - // if( ! parents[i].expanded ){ return false; } - // } - // return true; - // }, - /** Expand all keys that */ + /** [Not Implemented] */ nodeLoadKeyPath: function(ctx, keyPathList) { // TODO: implement and improve - // http://code.google.com/p/fancytree/issues/detail?id=222 + // http://code.google.com/p/dynatree/issues/detail?id=222 }, - /** Expand all parents.*/ - nodeMakeVisible: function(ctx) { - // TODO: also scroll as neccessary: http://stackoverflow.com/questions/8938352/fancytree-how-to-scroll-to-active-node - // Do we need an extra parameter? - var i, l, - parents = ctx.node.getParentList(false, false); - for(i=0, l=parents.length; i<l; i++){ - parents[i].setExpanded(true); - } - }, /** * Remove a single direct child of ctx.node. * @param {EventData} ctx * @param {FancytreeNode} childNode dircect child of ctx.node */ @@ -2277,11 +2474,11 @@ node = ctx.node, opts = ctx.options, subCtx = $.extend({}, ctx, {node: childNode}), children = node.children; - FT.debug("nodeRemoveChild()", node.toString(), childNode.toString()); + // FT.debug("nodeRemoveChild()", node.toString(), childNode.toString()); if( children.length === 1 ) { _assert(childNode === children[0]); return this.nodeRemoveChildren(ctx); } @@ -2298,10 +2495,11 @@ _assert(idx >= 0); // Unlink to support GC childNode.visit(function(n){ n.parent = null; }, true); + this._callHook("treeRegisterNode", this, false, childNode); if ( opts.removeNode ){ opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx); } // remove from child list children.splice(idx, 1); @@ -2310,11 +2508,11 @@ * @param {EventData} ctx */ nodeRemoveChildMarkup: function(ctx) { var node = ctx.node; - FT.debug("nodeRemoveChildMarkup()", node.toString()); + // FT.debug("nodeRemoveChildMarkup()", node.toString()); // TODO: Unlink attr.ftnode to support GC if(node.ul){ if( node.isRoot() ) { $(node.ul).empty(); } else { @@ -2329,15 +2527,16 @@ /**Remove all descendants of ctx.node. * @param {EventData} ctx */ nodeRemoveChildren: function(ctx) { var subCtx, + tree = ctx.tree, node = ctx.node, children = node.children, opts = ctx.options; - FT.debug("nodeRemoveChildren()", node.toString()); + // FT.debug("nodeRemoveChildren()", node.toString()); if(!children){ return; } if( this.activeNode && this.activeNode.isDescendantOf(node)){ this.activeNode.setActive(false); // TODO: don't fire events @@ -2350,45 +2549,49 @@ // Unlink children to support GC // TODO: also delete this.children (not possible using visit()) subCtx = $.extend({}, ctx); node.visit(function(n){ n.parent = null; + tree._callHook("treeRegisterNode", tree, false, n); 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; + if( node.lazy ){ + // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes + node.children = []; + } else{ + node.children = null; + } 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()); + // 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 `<li><span>..</span> .. </li>` tags for this node. + * Create `&lt;li>&lt;span>..&lt;/span> .. &lt;/li>` 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! + * parent. * <code> * <li id='KEY' ftnode=NODE> * <span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'> * <span class="fancytree-expander"></span> * <span class="fancytree-checkbox"></span> // only present in checkbox mode @@ -2400,14 +2603,14 @@ * <li id='KEY' ftnode=NODE> child2 ... </li> * </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 {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. * @@ -2424,14 +2627,14 @@ 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 + // 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) ) ){ @@ -2483,10 +2686,11 @@ if ( opts.createNode ){ opts.createNode.call(tree, {type: "createNode"}, ctx); } }else{ // this.nodeRenderTitle(ctx); + this.nodeRenderStatus(ctx); } // Allow tweaking after node state was rendered if ( opts.renderNode ){ opts.renderNode.call(tree, {type: "renderNode"}, ctx); } @@ -2533,11 +2737,11 @@ childLI = node.ul.firstChild; for(i=0, l=children.length-1; i<l; i++) { childNode1 = children[i]; childNode2 = childLI.ftnode; if( childNode1 !== childNode2 ) { - node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2); + // node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2); node.ul.insertBefore(childNode1.li, childNode2.li); } else { childLI = childLI.nextSibling; } } @@ -2550,19 +2754,22 @@ this.nodeRemoveChildMarkup(ctx); } } if( !isRootNode ){ // Update element classes according to node state - this.nodeRenderStatus(ctx); + // this.nodeRenderStatus(ctx); // Finally add the whole structure to the DOM, so the browser can render if(firstTime){ parent.ul.appendChild(node.li); } } }, /** Create HTML for the node's outer <span> (expander, checkbox, icon, and title). + * + * nodeRenderStatus() is implied. * @param {EventData} ctx + * @param {string} [title] optinal new title */ nodeRenderTitle: function(ctx, title) { // set node connector images, links and text var id, imageSrc, nodeTitle, role, tabindex, tooltip, node = ctx.node, @@ -2598,11 +2805,11 @@ }else{ ares.push("<span class='fancytree-expander'></span>"); } } // Checkbox mode - if( opts.checkbox && node.hideCheckbox !== true && !node.isStatusNode ) { + if( opts.checkbox && node.hideCheckbox !== true && !node.isStatusNode() ) { if(aria){ ares.push("<span role='checkbox' class='fancytree-checkbox'></span>"); }else{ ares.push("<span class='fancytree-checkbox'></span>"); } @@ -2626,21 +2833,23 @@ 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, "&quot;") + "'" : ""; + tooltip = node.tooltip ? " title='" + FT.escapeHtml(node.tooltip) + "'" : ""; id = aria ? " id='ftal_" + node.key + "'" : ""; role = aria ? " role='treeitem'" : ""; 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 CSS classes + this.nodeRenderStatus(ctx); }, /** Update element classes according to node state. * @param {EventData} ctx */ nodeRenderStatus: function(ctx) { @@ -2666,11 +2875,11 @@ cnList.push(cn.node); if( tree.activeNode === node ){ cnList.push(cn.active); // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); // tree.$container.removeAttr("tabindex"); - }else{ + // }else{ // $(">span.fancytree-title", statusElem).removeAttr("tabindex"); // tree.$container.attr("tabindex", "0"); } if( tree.focusNode === node ){ cnList.push(cn.focused); @@ -2710,10 +2919,16 @@ cnList.push(cn.lazy); } if( node.partsel ){ cnList.push(cn.partsel); } + if( node._isLoading ){ + cnList.push(cn.loading); + } + if( node._error ){ + cnList.push(cn.error); + } if( node.selected ){ cnList.push(cn.selected); if(aria){ $ariaElem.attr("aria-selected", true); } @@ -2753,30 +2968,31 @@ * 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 {object} [opts] additional options. Defaults to {} + * @param {object} [opts] additional options. Defaults to {noEvents: false} */ 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, + noEvents = (callOpts.noEvents === true), isActive = (node === tree.activeNode); // flag defaults to true flag = (flag !== false); - node.debug("nodeSetActive", flag); + // node.debug("nodeSetActive", flag); if(isActive === flag){ // Nothing to do return _getResolvedPromise(node); - }else if(flag && this._triggerNodeEvent("beforeActivate", node, ctx.originalEvent) === false ){ + }else if(flag && !noEvents && this._triggerNodeEvent("beforeActivate", node, ctx.originalEvent) === false ){ // Callback returned false return _getRejectedPromise(node, ["rejected"]); } if(flag){ if(tree.activeNode){ @@ -2784,55 +3000,62 @@ 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.nodeMakeVisible(ctx); + node.makeVisible(); } tree.activeNode = node; tree.nodeRenderStatus(ctx); tree.nodeSetFocus(ctx); - tree._triggerNodeEvent("activate", node); + if( !noEvents ) { + tree._triggerNodeEvent("activate", node, ctx.originalEvent); + } }else{ _assert(tree.activeNode === node, "node was not active (inconsistency)"); tree.activeNode = null; this.nodeRenderStatus(ctx); - ctx.tree._triggerNodeEvent("deactivate", node); + if( !noEvents ) { + ctx.tree._triggerNodeEvent("deactivate", node, ctx.originalEvent); + } } }, /** Expand or collapse node, return Deferred.promise. * * @param {EventData} ctx * @param {boolean} [flag=true] - * @param {object} [opts] additional options. Defaults to {noAnimation: false} + * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: 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, callOpts) { callOpts = callOpts || {}; var _afterLoad, dfd, i, l, parents, prevAC, node = ctx.node, tree = ctx.tree, opts = ctx.options, - noAnimation = callOpts.noAnimation === true; + noAnimation = (callOpts.noAnimation === true), + noEvents = (callOpts.noEvents === true); // flag defaults to true flag = (flag !== false); - node.debug("nodeSetExpanded(" + flag + ")"); + // node.debug("nodeSetExpanded(" + flag + ")"); if((node.expanded && flag) || (!node.expanded && !flag)){ // Nothing to do - node.debug("nodeSetExpanded(" + 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"]); + // return _getRejectedPromise(node, ["empty"]); + return _getResolvedPromise(node); }else if( !flag && node.getLevel() < opts.minExpandLevel ) { // Prevent collapsing locked levels return _getRejectedPromise(node, ["locked"]); - }else if ( this._triggerNodeEvent("beforeExpand", node, ctx.originalEvent) === false ){ + }else if ( !noEvents && 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() ) { @@ -2855,14 +3078,21 @@ opts.autoCollapse = prevAC; } } // Trigger expand/collapse after expanding dfd.done(function(){ - ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); if( opts.autoScroll && !noAnimation ) { // Scroll down to last child, but keep current node visible - node.getLastChild().scrollIntoView(true, node); + node.getLastChild().scrollIntoView(true, node).always(function(){ + if( !noEvents ) { + ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); + } + }); + } else { + if( !noEvents ) { + ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); + } } }); // vvv Code below is executed after loading finished: _afterLoad = function(callback){ @@ -2894,13 +3124,13 @@ node.ul.style.display = ( node.expanded || !parent ) ? "" : "none"; } else { duration = opts.fx.duration || 200; easing = opts.fx.easing; - node.debug("nodeSetExpanded: animate start..."); + // node.debug("nodeSetExpanded: animate start..."); $(node.ul).animate(opts.fx, duration, easing, function(){ - node.debug("nodeSetExpanded: animate done"); + // node.debug("nodeSetExpanded: animate done"); callback(); }); return; } } @@ -2908,23 +3138,23 @@ }; // ^^^ Code above is executed after loading finshed. // Load lazy nodes, if any. Then continue with _afterLoad() if(flag && node.lazy && node.hasChildren() === undefined){ - node.debug("nodeSetExpanded: load start..."); - node.lazyLoad().done(function(){ - node.debug("nodeSetExpanded: load done"); + // node.debug("nodeSetExpanded: load start..."); + node.load().done(function(){ + // node.debug("nodeSetExpanded: load done"); if(dfd.notifyWith){ // requires jQuery 1.6+ dfd.notifyWith(node, ["loaded"]); } _afterLoad(function () { dfd.resolveWith(node); }); }).fail(function(errMsg){ _afterLoad(function () { dfd.rejectWith(node, ["load failed (" + errMsg + ")"]); }); }); /* - var source = tree._triggerNodeEvent("lazyload", node, ctx.originalEvent); - _assert(typeof source !== "boolean", "lazyload event must return source in data.result"); + var source = tree._triggerNodeEvent("lazyLoad", node, ctx.originalEvent); + _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result"); node.debug("nodeSetExpanded: load start..."); this._callHook("nodeLoadChildren", ctx, source).done(function(){ node.debug("nodeSetExpanded: load done"); if(dfd.notifyWith){ // requires jQuery 1.6+ dfd.notifyWith(node, ["loaded"]); @@ -2935,29 +3165,29 @@ }); */ }else{ _afterLoad(function () { dfd.resolveWith(node); }); } - node.debug("nodeSetExpanded: returns"); + // node.debug("nodeSetExpanded: returns"); return dfd.promise(); }, - /** + /** Focus ot blur this node. * @param {EventData} ctx * @param {boolean} [flag=true] */ nodeSetFocus: function(ctx, flag) { - ctx.node.debug("nodeSetFocus(" + flag + ")"); + // ctx.node.debug("nodeSetFocus(" + flag + ")"); var ctx2, tree = ctx.tree, node = ctx.node; flag = (flag !== false); // Blur previous node if any if(tree.focusNode){ if(tree.focusNode === node && flag){ - node.debug("nodeSetFocus(" + flag + "): nothing to do"); + // node.debug("nodeSetFocus(" + flag + "): nothing to do"); return; } ctx2 = $.extend({}, ctx, {node: tree.focusNode}); tree.focusNode = null; this._triggerNodeEvent("blur", ctx2); @@ -2968,11 +3198,12 @@ if( !this.hasFocus() ){ node.debug("nodeSetFocus: forcing container focus"); // Note: we pass _calledByNodeSetFocus=true this._callHook("treeSetFocus", ctx, true, true); } - this.nodeMakeVisible(ctx); + // this.nodeMakeVisible(ctx); + node.makeVisible(); tree.focusNode = node; // node.debug("FOCUS..."); // $(node.span).find(".fancytree-title").focus(); this._triggerNodeEvent("focus", ctx); // if(ctx.options.autoActivate){ @@ -3030,73 +3261,83 @@ * @param status * @param message * @param details */ nodeSetStatus: function(ctx, status, message, details) { - var _clearStatusNode, _setStatusNode, - node = ctx.node, - tree = ctx.tree, - cn = ctx.options._classNames; + var node = ctx.node, + tree = ctx.tree; + // cn = ctx.options._classNames; - _clearStatusNode = function() { + function _clearStatusNode() { + // Remove dedicated dummy node, if any var firstChild = ( node.children ? node.children[0] : null ); - if ( firstChild && firstChild.isStatusNode ) { + if ( firstChild && firstChild.isStatusNode() ) { try{ // I've seen exceptions here with loadKeyPath... if(node.ul){ node.ul.removeChild(firstChild.li); - firstChild.li = null; // avoid leaks (issue 215) + firstChild.li = null; // avoid leaks (DT issue 215) } }catch(e){} if( node.children.length === 1 ){ node.children = []; }else{ node.children.shift(); } } - }; - _setStatusNode = function(data) { + } + function _setStatusNode(data, type) { + // Create/modify the dedicated dummy node for 'loading...' or + // 'error!' status. (only called for direct child of the invisible + // system root) var firstChild = ( node.children ? node.children[0] : null ); - if ( firstChild && firstChild.isStatusNode ) { + if ( firstChild && firstChild.isStatusNode() ) { $.extend(firstChild, data); tree._callHook("nodeRender", firstChild); } else { data.key = "_statusNode"; node._setChildren([data]); - node.children[0].isStatusNode = true; + node.children[0].statusNodeType = type; tree.render(); } return node.children[0]; - }; - switch(status){ + } + + switch( status ){ case "ok": _clearStatusNode(); - $(node.span).removeClass(cn.loading); - $(node.span).removeClass(cn.error); + // $(node.span).removeClass(cn.loading).removeClass(cn.error); + node._isLoading = false; + node._error = null; + node.renderStatus(); break; case "loading": - $(node.span).removeClass(cn.error); - $(node.span).addClass(cn.loading); - if(!node.parent){ + // $(node.span).removeClass(cn.error).addClass(cn.loading); + if( !node.parent ) { _setStatusNode({ title: tree.options.strings.loading + (message ? " (" + message + ") " : ""), tooltip: details, extraClasses: "fancytree-statusnode-wait" - }); + }, status); } + node._isLoading = true; + node._error = null; + node.renderStatus(); break; case "error": - $(node.span).removeClass(cn.loading); - $(node.span).addClass(cn.error); + // $(node.span).removeClass(cn.loading).addClass(cn.error); _setStatusNode({ title: tree.options.strings.loadError + (message ? " (" + message + ") " : ""), tooltip: details, extraClasses: "fancytree-statusnode-error" - }); + }, status); + node._isLoading = false; + node._error = { message: message, details: details }; + node.renderStatus(); break; default: - $.error("invalid status " + status); + $.error("invalid node status " + status); } }, /** * * @param {EventData} ctx @@ -3138,11 +3379,11 @@ //this.debug("Fancytree.treeInit()"); this.treeLoad(ctx); }, /** Parse Fancytree from source, as configured in the options. * @param {EventData} ctx - * @param {object} [source] new source + * @param {object} [source] optional new source (use last data otherwise) */ treeLoad: function(ctx, source) { var type, $ul, tree = ctx.tree, $container = ctx.widget.element, @@ -3160,10 +3401,12 @@ switch(type){ case "html": $ul = $container.find(">ul:first"); $ul.addClass("ui-fancytree-source ui-helper-hidden"); source = $.ui.fancytree.parseHtml($ul); + // allow to init tree.data.foo from <ul data-foo=''> + this.data = $.extend(this.data, _getElementDataAsDict($ul)); break; case "json": // $().addClass("ui-helper-hidden"); source = $.parseJSON($container.text()); if(source.children){ @@ -3191,16 +3434,27 @@ tree.render(); tree._triggerTreeEvent("init", false); }); return dfd; }, + /** Node was inserted into or removed from the tree. + * @param {EventData} ctx + * @param {boolean} add + * @param {FancytreeNode} node + */ + treeRegisterNode: function(ctx, add, node) { + }, + /** Widget got focus. + * @param {EventData} ctx + * @param {boolean} [flag=true] + */ treeSetFocus: function(ctx, flag, _calledByNodeSetFocus) { flag = (flag !== false); - this.debug("treeSetFocus(" + flag + "), _calledByNodeSetFocus: " + _calledByNodeSetFocus); - this.debug(" focusNode: " + this.focusNode); - this.debug(" activeNode: " + this.activeNode); + // 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"); } @@ -3209,24 +3463,25 @@ /* ****************************************************************************** * jQuery UI widget boilerplate */ + /** - * 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); + * The plugin (derrived from <a href=" http://api.jqueryui.com/jQuery.widget/">jQuery.Widget</a>).<br> + * This constructor is not called directly. Use `$(selector).fancytree({})` + * to initialize the plugin instead.<br> + * <pre class="sh_javascript sunlight-highlight-javascript">// Access widget methods and members: + * var tree = $("#tree").fancytree("getTree"); + * var node = $("#tree").fancytree("getActiveNode", "1234"); * </pre> + * + * @mixin Fancytree_Widget */ + $.widget("ui.fancytree", - /** @lends ui.fancytree# */ + /** @lends Fancytree_Widget# */ { /**These options will be used as defaults * @type {FancytreeOptions} */ options: @@ -3279,11 +3534,11 @@ lastsib: "fancytree-lastsib", loading: "fancytree-loading", error: "fancytree-error" }, // events - lazyload: null, + lazyLoad: null, postProcess: null }, /* Set up the widget, Called on first $().fancytree() */ _create: function() { this.tree = new Fancytree(this); @@ -3464,57 +3719,67 @@ 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 +// // var _ = null; // DT issue 117 // TODO // $.error(e); } finally { tree.phase = prevPhase; } }); }, - /** @returns {FancytreeNode} the active node or null */ + /** Return the active node or null. + * @returns {FancytreeNode} + */ getActiveNode: function() { return this.tree.activeNode; }, - /** + /** Return the matching node or null. * @param {string} key - * @returns {FancytreeNode} the matching node or null + * @returns {FancytreeNode} */ getNodeByKey: function(key) { return this.tree.getNodeByKey(key); }, - /** @returns {FancytreeNode} the invisible system root node */ + /** Return the invisible system root node. + * @returns {FancytreeNode} + */ getRootNode: function() { return this.tree.rootNode; }, - /** @returns {Fancytree} the current tree instance */ + /** Return the current tree instance. + * @returns {Fancytree} + */ 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. - * - * @example: - * alert(""version: " + $.ui.fancytree.version); +/** + * Static members in the `$.ui.fancytree` namespace.<br> + * <br> + * <pre class="sh_javascript sunlight-highlight-javascript">// Access static members: * var node = $.ui.fancytree.getNode(element); + * alert($.ui.fancytree.version); + * </pre> + * + * @mixin Fancytree_Static */ $.extend($.ui.fancytree, - /** @lends ui.fancytree */ + /** @lends Fancytree_Static# */ { /** @type {string} */ - version: "2.0.0-6", + version: "2.0.0-11", // Set to semver by 'grunt release' /** @type {string} */ - buildType: "release", + buildType: "production", // Set to 'production' by 'grunt build' /** @type {int} */ - debugLevel: 1, // used by $.ui.fancytree.debug() and as default for tree.options.debugLevel + debugLevel: 1, // Set to 1 by 'grunt build' + // Used by $.ui.fancytree.debug() and as default for tree.options.debugLevel _nextId: 1, _nextNodeKey: 1, _extensions: {}, // focusTree: null, @@ -3526,23 +3791,52 @@ /* 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) }, + /** Throw an error if condition fails (debug method). + * @param {boolean} cond + * @param {string} msg + */ assert: function(cond, msg){ return _assert(cond, msg); }, + /** Write message to console if debugLevel >= 2 + * @param {string} msg + */ debug: function(msg){ /*jshint expr:true */ ($.ui.fancytree.debugLevel >= 2) && consoleApply("log", arguments); }, + /** Write error message to console. + * @param {string} msg + */ error: function(msg){ consoleApply("error", arguments); }, + /** Convert &lt;, &gt;, &amp;, &quot;, &#39;, &#x2F; to the equivalent entitites. + * + * @param {string} s + * @returns {string} + */ + escapeHtml: function(s){ + return ("" + s).replace(/[&<>"'\/]/g, function (s) { + return ENTITY_MAP[s]; + }); + }, + /** Inverse of escapeHtml(). + * + * @param {string} s + * @returns {string} + */ + unescapeHtml: function(s){ + var e = document.createElement("div"); + e.innerHTML = s; + return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue; + }, /** 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; @@ -3561,16 +3855,16 @@ // 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) ){ + }else if( /\bfancytree-checkbox\b/.test(tcn) || /\bfancytree-radio\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) + // TODO: (http://code.google.com/p/dynatree/issues/detail?id=93) // res.type = this._getTypeForOuterNodeEvent(event); res.type = "title"; } return res; }, @@ -3610,10 +3904,13 @@ } ... return null; }, */ + /** Write message to console if debugLevel >= 1 + * @param {string} msg + */ info: function(msg){ /*jshint expr:true */ ($.ui.fancytree.debugLevel >= 1) && consoleApply("info", arguments); }, /** @@ -3623,17 +3920,16 @@ * @returns {NodeData[]} */ parseHtml: function($ul) { // TODO: understand this: /*jshint validthis:true */ - var $children = $ul.find(">li"), - extraClasses, i, l, iPos, tmp, classes, className, + var extraClasses, i, l, iPos, tmp, tmp2, classes, className, + $children = $ul.find(">li"), children = []; -// that = this; $children.each(function() { - var allData, jsonData, + var allData, $li = $(this), $liSpan = $li.find(">span:first", this), $liA = $liSpan.length ? null : $li.find(">a:first"), d = { tooltip: null, data: {} }; @@ -3683,28 +3979,24 @@ tmp = $li.attr("id"); if( tmp ){ d.key = tmp; } // Add <li data-NAME='...'> as node.data.NAME - // See http://api.jquery.com/data/#data-html5 - allData = $li.data(); -// alert("d: " + JSON.stringify(allData)); + allData = _getElementDataAsDict($li); if(allData && !$.isEmptyObject(allData)) { - // Special handling for <li data-json='...'> - jsonData = allData.json; - delete allData.json; - $.extend(d.data, allData); - // If a 'data-json' attribute is present, evaluate and add to node.data - if(jsonData) { -// alert("$li.data()" + JSON.stringify(jsonData)); - // <li data-json='...'> is already returned as object - // see http://api.jquery.com/data/#data-html5 - $.extend(d.data, jsonData); + // #56: Allow to set special node.attributes from data-... + for(i=0, l=NODE_ATTRS.length; i<l; i++){ + tmp = NODE_ATTRS[i]; + tmp2 = allData[tmp]; + if( tmp2 != null ) { + delete allData[tmp]; + d[tmp] = tmp2; + } } + // All other data-... goes to node.data... + $.extend(d.data, allData); } -// that.debug("parse ", d); -// var childNode = parentTreeNode.addChild(data); // Recursive reading of child nodes, if LI tag contains an UL tag $ul = $li.find(">ul:first"); if( $ul.length ) { d.children = $.ui.fancytree.parseHtml($ul); }else{ @@ -3715,47 +4007,213 @@ }); return children; }, /** Add Fancytree extension definition to the list of globally available extensions. * - * @param {Object} definition + * @param {object} 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; }, + /** Write warning message to console. + * @param {string} msg + */ warn: function(msg){ consoleApply("warn", arguments); } }); -// Use $.ui.fancytree.debugLevel as default for tree.options.debugLevel -//$.ui.fancytree.debug($.ui.fancytree.prototype); -//$.ui.fancytree.prototype.options.debugLevel = $.ui.fancytree.debugLevel; +}(jQuery, window, document)); +// Extending Fancytree +// =================== +// +// See also the [live demo](http://wwwendt.de/tech/fancytree/demo/sample-ext-childcounter.html) of this code. +// +// Every extension should have a comment header containing some information +// about the author, copyright and licensing. Also a pointer to the latest +// source code. +// Prefix with `/*!` so the comment is not removed by the minifier. -/* ***************************************************************************** - * Register AMD +/*! + * jquery.fancytree.childcounter.js + * + * Add a child counter bubble to tree nodes. + * (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-11 + * @date 2014-04-27T22:28 */ -// http://stackoverflow.com/questions/10918063/how-to-make-a-jquery-plugin-loadable-with-requirejs -// if ( typeof define === "function" && define.amd && define.amd.jQuery ) { -// define( "jquery", [], function () { return jQuery; } ); -// } +// To keep the global namespace clean, we wrap everything in a closure -// TODO: maybe like so:? -// https://raw.github.com/malsup/blockui/master/jquery.blockUI.js -/* -if( typeof define === "function" && define.amd ) { - define( ["jquery"], function () { - return jQuery.ui.fancytree; - }); -} -*/ -}(jQuery, window, document)); +;(function($, undefined) { +// Consider to use [strict mode](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) +"use strict"; + +// The [coding guidelines](http://contribute.jquery.org/style-guide/js/) +// require jshint compliance. +// But for this sample, we want to allow unused variables for demonstration purpose. + +/*jshint unused:false */ + + +// Adding methods +// -------------- + +// New member functions can be added to the `Fancytree` class. +// This function will be available for every tree instance. +// +// var tree = $("#tree").fancytree("getTree"); +// tree.countSelected(false); + +$.ui.fancytree._FancytreeClass.prototype.countSelected = function(topOnly){ + var tree = this, + treeOptions = tree.options; + return tree.getSelectedNodes(topOnly).length; +}; + + +// The `FancytreeNode` class can also be easily extended. This would be called +// like +// +// node.toUpper(); + +$.ui.fancytree._FancytreeNodeClass.prototype.toUpper = function(){ + var node = this; + return node.setTitle(node.title.toUpperCase()); +}; + + +// Finally, we can extend the widget API and create functions that are called +// like so: +// +// $("#tree").fancytree("widgetMethod1", "abc"); + +$.ui.fancytree.prototype.widgetMethod1 = function(arg1){ + var tree = this.tree; + return arg1; +}; + + +// Register a Fancytree extension +// ------------------------------ +// A full blown extension, extension is available for all trees and can be +// enabled like so (see also the [live demo](http://wwwendt.de/tech/fancytree/demo/sample-ext-childcounter.html)): +// +// <script src="../src/jquery.fancytree.js" type="text/javascript"></script> +// <script src="../src/jquery.fancytree.childcounter.js" type="text/javascript"></script> +// ... +// +// $("#tree").fancytree({ +// extensions: ["childcounter"], +// childcounter: { +// hideExpanded: true +// }, +// ... +// }); +// + + +/* 'childcounter' extension */ +$.ui.fancytree.registerExtension({ +// Every extension must be registered by a unique name. + name: "childcounter", +// Version information should be compliant with [semver](http://semver.org) + version: "1.0.0", + +// Extension specific options and their defaults. +// This options will be available as `tree.options.childcounter.hideExpanded` + + options: { + deep: true, + hideZeros: true, + hideExpanded: false + }, + +// Attributes other than `options` (or functions) can be defined here, and +// will be added to the tree.ext.EXTNAME namespace, in this case `tree.ext.childcounter.foo`. +// They can also be accessed as `this._local.foo` from within the extension +// methods. + foo: 42, + +// Local functions are prefixed with an underscore '_'. +// Callable as `this._local._appendCounter()`. + + _appendCounter: function(bar){ + var tree = this; + }, + +// **Override virtual methods for this extension.** +// +// Fancytree implements a number of 'hook methods', prefixed by 'node...' or 'tree...'. +// with a `ctx` argument (see [EventData](http://www.wwwendt.de/tech/fancytree/doc/jsdoc/global.html#EventData) +// for details) and an extended calling context:<br> +// `this` : the Fancytree instance<br> +// `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME)<br> +// `this._super`: the virtual function that was overridden (member of previous extension or Fancytree) +// +// See also the [complete list of available hook functions](http://www.wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Hooks.html). + + /* Init */ +// `treeInit` is triggered when a tree is initalized. We can set up classes or +// bind event handlers here... + treeInit: function(ctx){ + var tree = this, // same as ctx.tree, + opts = ctx.options, + extOpts = ctx.options.childcounter; +// Optionally check for dependencies with other extensions + // this._requireExtension("glyph", false, false); +// Call the base implementation + this._super(ctx); +// Add a class to the tree container + this.$container.addClass("fancytree-ext-childcounter"); + }, + +// Destroy this tree instance (we only call the default implementation, so +// this method could as well be omitted). + + treeDestroy: function(ctx){ + this._super(ctx); + }, + +// Overload the `renderTitle` hook, to append a counter badge + nodeRenderTitle: function(ctx, title) { + var node = ctx.node, + extOpts = ctx.options.childcounter, + count = (node.data.childCounter == null) ? node.countChildren(extOpts.deep) : +node.data.childCounter; +// Let the base implementation render the title + this._super(ctx, title); +// Append a counter badge + if( (count || ! extOpts.hideZeros) && (!node.isExpanded() || !extOpts.hideExpanded) ){ + $("span.fancytree-icon", node.span).append($("<span class='fancytree-childcounter'/>").text(count)); + } + }, +// Overload the `setExpanded` hook, so the counters are updated + nodeSetExpanded: function(ctx, flag, opts) { + var tree = ctx.tree, + node = ctx.node; +// Let the base implementation expand/collapse the node, then redraw the title +// after the animation has finished + return this._super(ctx, flag, opts).always(function(){ + tree.nodeRenderTitle(ctx); + }); + } + +// End of extension definition +}); +// End of namespace closure +}(jQuery)); + /*! * jquery.fancytree.dnd.js * * Drag-and-drop support. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) @@ -3763,12 +4221,12 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ ;(function($, window, document, undefined) { "use strict"; @@ -3788,16 +4246,16 @@ * Drag and drop support */ function _initDragAndDrop(tree) { var dnd = tree.options.dnd || null; // Register 'connectToFancytree' option with ui.draggable - if(dnd /*&& (dnd.dragStart || dnd.dragDrop)*/) { + if( dnd ) { _registerDnd(); } // Attach ui.draggable to this Fancytree instance if(dnd && dnd.dragStart ) { - tree.widget.element.draggable({ + tree.widget.element.draggable($.extend({ addClasses: false, appendTo: "body", containment: false, delay: 0, distance: 4, @@ -3809,25 +4267,25 @@ // Delegate draggable.start, drag, and stop events to our handler connectToFancytree: true, // Let source tree create the helper element helper: function(event) { var sourceNode = $.ui.fancytree.getNode(event.target); - if(!sourceNode){ // issue 211 - // TODO: remove this hint, when we understand when it happens + if(!sourceNode){ // Dynatree issue 211 + // might happen, if dragging a table *header* return "<div>ERROR?: helper requested but sourceNode not found</div>"; } return sourceNode.tree.ext.dnd._onDragEvent("helper", sourceNode, null, event, null, null); }, start: function(event, ui) { -// var sourceNode = $.ui.fancytree.getNode(event.target); - // don't return false if sourceNode == null (see issue 268) + var sourceNode = ui.helper.data("ftSourceNode"); + return !!sourceNode; // Abort dragging if no node could be found } - }); + }, tree.options.dnd.draggable)); } // Attach ui.droppable to this Fancytree instance if(dnd && dnd.dragDrop) { - tree.widget.element.droppable({ + tree.widget.element.droppable($.extend({ addClasses: false, tolerance: "intersect", greedy: false /* , @@ -3848,11 +4306,11 @@ }, over: function(event, ui) { logMsg("droppable - over", event, ui); } */ - }); + }, tree.options.dnd.droppable)); } } //--- Extend ui.draggable event handling -------------------------------------- @@ -3961,25 +4419,28 @@ */ $.ui.fancytree.registerExtension( { name: "dnd", - version: "0.0.1", + version: "0.1.0", // Default options for this extension. options: { // Make tree nodes draggable: - dragStart: null, // Callback(sourceNode, data), return true, to enable dnd - dragStop: null, // Callback(sourceNode, data) + dragStart: null, // Callback(sourceNode, data), return true, to enable dnd + dragStop: null, // Callback(sourceNode, data) // helper: null, // Make tree nodes accept draggables autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering. preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. preventRecursiveMoves: true, // Prevent dropping nodes on own descendants - dragEnter: null, // Callback(targetNode, data) - dragOver: null, // Callback(targetNode, data) - dragDrop: null, // Callback(targetNode, data) - dragLeave: null // Callback(targetNode, data) + dragEnter: null, // Callback(targetNode, data) + dragOver: null, // Callback(targetNode, data) + dragDrop: null, // Callback(targetNode, data) + dragLeave: null, // Callback(targetNode, data) + // + draggable: null, // Additional options passed to jQuery draggable + droppable: null // Additional options passed to jQuery droppable }, // Override virtual methods for this extension. // `this` : Fancytree instance // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) treeInit: function(ctx){ @@ -4148,11 +4609,11 @@ // logMsg("helper=%o", $helper); // logMsg("helper.sourceNode=%o", $helper.data("ftSourceNode")); res = $helper; break; case "start": - if( node.isStatusNode ) { + if( node.isStatusNode() ) { res = false; } else if(dnd.dragStart) { res = dnd.dragStart(node, ctx); } if(res === false) { @@ -4300,27 +4761,26 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ -/** - * @module fancytree/edit - */ ;(function($, window, document, undefined) { "use strict"; /******************************************************************************* * Private functions and variables */ -var isMac = /Mac/.test(navigator.platform) +var isMac = /Mac/.test(navigator.platform), + escapeHtml = $.ui.fancytree.escapeHtml, + unescapeHtml = $.ui.fancytree.unescapeHtml; // 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", @@ -4332,13 +4792,13 @@ // }, // 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++ ) { // } @@ -4381,11 +4841,11 @@ }); // Replace node with <input> $input = $("<input />", { "class": "fancytree-edit-input", - value: prevTitle + value: unescapeHtml(prevTitle) }); if ( instOpts.adjustWidthOfs != null ) { $input.width($title.width() + instOpts.adjustWidthOfs); } if ( instOpts.inputCss != null ) { @@ -4454,11 +4914,11 @@ .unbind(); // Unbind outer-click handler $(document).off(".fancytree-edit"); if( doSave ) { - node.setTitle( newVal ); + node.setTitle( escapeHtml(newVal) ); }else{ node.renderTitle(); } // Re-enable mouse and keyboard handling tree.widget._bind(); @@ -4471,17 +4931,17 @@ return true; }; $.ui.fancytree._FancytreeNodeClass.prototype.startEdit = function(){ - this.warn("FancytreeNode.startEdit() is deprecated. Use .editStart() instead."); + this.warn("FancytreeNode.startEdit() is deprecated since 2014-01-04. Use .editStart() instead."); return this.editStart.apply(this, arguments); }; $.ui.fancytree._FancytreeNodeClass.prototype.endEdit = function(){ - this.warn("FancytreeNode.endEdit() is deprecated. Use .editEnd() instead."); + this.warn("FancytreeNode.endEdit() is deprecated since 2014-01-04. Use .editEnd() instead."); return this.editEnd.apply(this, arguments); }; ///** @@ -4620,12 +5080,12 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ ;(function($, window, document, undefined) { "use strict"; @@ -4638,28 +5098,37 @@ function _escapeRegex(str){ /*jshint regexdash:true */ return (str + "").replace(/([.?*+\^\$\[\]\\(){}|-])/g, "\\$1"); } +/* EXT-TABLE: Show/hide all rows that are structural descendants of `parent`. */ +// function setChildRowVisibility(parent, flag) { +// parent.visit(function(node){ +// var tr = node.tr; +// if(tr){ +// tr.style.display = flag ? "" : "none"; +// } +// node.debug(flag ? "SHOW" : "HIDE"); +// if(!node.expanded){ +// return "skip"; +// } +// }); +// } /** - * Dimm or hide nodes. + * [ext-filter] Dimm or hide nodes. * * @param {function | string} filter * @returns {integer} count - * @lends Fancytree.prototype + * @alias Fancytree#applyFilter * @requires jquery.fancytree.filter.js */ $.ui.fancytree._FancytreeClass.prototype.applyFilter = function(filter){ var match, re, count = 0, + hideMode = this.options.filter.mode === "hide", leavesOnly = this.options.filter.leavesOnly; - // Reset current filter - this.visit(function(node){ - delete node.match; - delete node.subMatch; - }); // Default to 'match title substring (not case sensitive)' if(typeof filter === "string"){ match = _escapeRegex(filter); // make sure a '.' is treated literally re = new RegExp(".*" + match + ".*", "i"); @@ -4668,51 +5137,62 @@ }; } this.enableFilter = true; this.$div.addClass("fancytree-ext-filter"); - if( this.options.filter.mode === "hide"){ + if( hideMode ){ this.$div.addClass("fancytree-ext-filter-hide"); + } else { + this.$div.addClass("fancytree-ext-filter-dimm"); } + // Reset current filter this.visit(function(node){ +// node.hide = hideMode && true; + delete node.match; + delete node.subMatch; + }); + // Adjust node.hide, .match, .subMatch flags + this.visit(function(node){ if ((!leavesOnly || node.children == null) && filter(node)) { count++; +// node.hide = false; node.match = true; node.visitParents(function(p){ +// p.hide = false; p.subMatch = true; }); } }); + // Redraw this.render(); return count; }; /** - * Reset the filter. + * [ext-filter] Reset the filter. * - * @lends Fancytree.prototype + * @alias Fancytree#applyFilter * @requires jquery.fancytree.filter.js */ $.ui.fancytree._FancytreeClass.prototype.clearFilter = function(){ this.visit(function(node){ +// delete node.hide; delete node.match; delete node.subMatch; - $(node.li).show(); }); - this.enableFilter = false; + this.$div.removeClass("fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide"); this.render(); - this.$div.removeClass("fancytree-ext-filter fancytree-ext-filter-hide"); }; /******************************************************************************* * Extension code */ $.ui.fancytree.registerExtension({ name: "filter", - version: "0.0.1", + version: "0.0.2", // Default options for this extension. options: { mode: "dimm", leavesOnly: false }, @@ -4727,43 +5207,172 @@ treeDestroy: function(ctx){ this._super(ctx); }, nodeRenderStatus: function(ctx) { // Set classes for current status - var visible, + var res, node = ctx.node, - opts = ctx.options, tree = ctx.tree, $span = $(node[tree.statusClassPropName]); + res = this._super(ctx); + if(!$span.length){ - return; // nothing to do, if node was not yet rendered + return res; // nothing to do, if node was not yet rendered } - this._super(ctx); if(!tree.enableFilter){ + return res; + } + $span.toggleClass("fancytree-match", !!node.match); + $span.toggleClass("fancytree-submatch", !!node.subMatch); + $span.toggleClass("fancytree-hide", !(node.match || node.subMatch)); + + // if(opts.filter.mode === "hide"){ + // // visible = !!(node.match || node.subMatch); + // visible = !node.hide; + // node.debug(node.title + ": visible=" + visible); + // if( node.li ) { + // $(node.li).toggle(visible); + // } else if( node.tr ) { + // // Show/hide all rows that are structural descendants of `parent` + // $(node.tr).toggle(visible); + // // if( !visible ) { + // // setChildRowVisibility(node, visible); + // // } + // } + // } + return res; + } +}); +}(jQuery, window, document)); + +/*! + * jquery.fancytree.glyph.js + * + * Use glyph fonts as instead of icon sprites. + * (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-11 + * @date 2014-04-27T22:28 + */ + +;(function($, window, document, undefined) { + +"use strict"; + +/* ***************************************************************************** + * Private functions and variables + */ + +function _getIcon(opts, type){ + return opts.map[type]; +} + +$.ui.fancytree.registerExtension({ + name: "glyph", + version: "0.0.2", + // Default options for this extension. + options: { + prefix: "icon-", + extra: null, + map: { + doc: "icon-file-alt", + docOpen: "icon-file-alt", + checkbox: "icon-check-empty", + checkboxSelected: "icon-check", + checkboxUnknown: "icon-check icon-muted", + error: "icon-exclamation-sign", + expanderClosed: "icon-caret-right", + expanderLazy: "icon-angle-right", + // expanderLazy: "icon-refresh icon-spin", + expanderOpen: "icon-caret-down", + folder: "icon-folder-close-alt", + folderOpen: "icon-folder-open-alt", + loading: "icon-refresh icon-spin", + // loading: "icon-spinner icon-spin" + noExpander: "" + }, + icon: null // TODO: allow callback here + }, + // Overide virtual methods for this extension. + // `this` : is this extension object + // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) + treeInit: function(ctx){ + var tree = ctx.tree; + this._super(ctx); + tree.$container.addClass("fancytree-ext-glyph"); + }, + nodeRenderStatus: function(ctx) { + var icon, span, + node = ctx.node, + opts = ctx.options.glyph, + // callback = opts.icon, + map = opts.map + // prefix = opts.prefix + // $span = $(node.span) + ; + + this._super(ctx); + + if( node.isRoot() ){ return; } - if( node.match ){ - $span.addClass("fancytree-match"); - }else{ - $span.removeClass("fancytree-match"); + + span = $("span.fancytree-expander", node.span).get(0); + if( span ){ + if( node.isLoading() ){ + icon = "loading"; + }else if( node.expanded ){ + icon = "expanderOpen"; + }else if( node.isUndefined() ){ + icon = "expanderLazy"; + }else if( node.hasChildren() ){ + icon = "expanderClosed"; + }else{ + icon = "noExpander"; + } + span.className = "fancytree-expander " + map[icon]; } - if( node.subMatch ){ - $span.addClass("fancytree-submatch"); + + span = $("span.fancytree-checkbox", node.tr || node.span).get(0); + if( span ){ + icon = node.selected ? "checkboxSelected" : (node.partsel ? "checkboxUnknown" : "checkbox"); + span.className = "fancytree-checkbox " + map[icon]; + } + + span = $("span.fancytree-icon", node.span).get(0); + if( span ){ + if( node.folder ){ + icon = node.expanded ? _getIcon(opts, "folderOpen") : _getIcon(opts, "folder"); + }else{ + icon = node.expanded ? _getIcon(opts, "docOpen") : _getIcon(opts, "doc"); + } + span.className = "fancytree-icon " + icon; + } + }, + nodeSetStatus: function(ctx, status, message, details) { + var span, + opts = ctx.options.glyph, + node = ctx.node; + + this._super(ctx, status, message, details); + + if(node.parent){ + span = $("span.fancytree-expander", node.span).get(0); }else{ - $span.removeClass("fancytree-submatch"); + span = $(".fancytree-statusnode-wait, .fancytree-statusnode-error", node[this.nodeContainerAttrName]).find("span.fancytree-expander").get(0); } - if(opts.filter.mode === "hide"){ - visible = !!(node.match || node.subMatch); - node.debug(node.title + ": visible=" + visible); - $(node.li).toggle(visible); - // TODO: handle ext-table. - // The following is too simple, since we have to hide all TRs that - // belong to that parent: - // if( node.tr ) { - // $(node.tr).toggle(visible); - // } + if( status === "loading"){ + // $("span.fancytree-expander", ctx.node.span).addClass(_getIcon(opts, "loading")); + span.className = "fancytree-expander " + _getIcon(opts, "loading"); + }else if( status === "error"){ + span.className = "fancytree-expander " + _getIcon(opts, "error"); } } }); }(jQuery, window, document)); @@ -4776,12 +5385,12 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ ;(function($, window, document, undefined) { "use strict"; @@ -4803,25 +5412,84 @@ "select-one": [KC.LEFT, KC.RIGHT], "select-multiple": [KC.LEFT, KC.RIGHT] }; +/* Calculate TD column index (considering colspans).*/ +function getColIdx($tr, $td) { + var colspan, + td = $td.get(0), + idx = 0; + + $tr.children().each(function () { + if( this === td ) { + return false; + } + colspan = $(this).prop("colspan"); + idx += colspan ? colspan : 1; + }); + return idx; +} + + +/* Find TD at given column index (considering colspans).*/ +function findTdAtColIdx($tr, colIdx) { + var colspan, + res = null, + idx = 0; + + $tr.children().each(function () { + if( idx >= colIdx ) { + res = $(this); + return false; + } + colspan = $(this).prop("colspan"); + idx += colspan ? colspan : 1; + }); + return res; +} + + +/* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */ function findNeighbourTd($target, keyCode){ - var $td = $target.closest("td"); + var $tr, colIdx, + $td = $target.closest("td"), + $tdNext = null; + switch( keyCode ){ case KC.LEFT: - return $td.prev(); + $tdNext = $td.prev(); + break; case KC.RIGHT: - return $td.next(); + $tdNext = $td.next(); + break; 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()); + $tr = $td.parent(); + colIdx = getColIdx($tr, $td); + while( true ) { + $tr = (keyCode === KC.UP) ? $tr.prev() : $tr.next(); + if( !$tr.length ) { + break; + } + // Skip hidden rows + if( $tr.is(":hidden") ) { + continue; + } + // Find adjacent cell in the same column + $tdNext = findTdAtColIdx($tr, colIdx); + // Skip cells that don't conatain a focusable element + if( $tdNext && $tdNext.find(":input").length ) { + break; + } + } + break; } - return null; + return $tdNext; } + /******************************************************************************* * Extension code */ $.ui.fancytree.registerExtension({ name: "gridnav", @@ -4885,11 +5553,11 @@ event = ctx.originalEvent, $target = $(event.target); // jQuery inputType = $target.is(":input:enabled") ? $target.prop("type") : null; - ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType); +// 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); @@ -4919,12 +5587,12 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ ;(function($, window, document, undefined) { "use strict"; @@ -4943,67 +5611,132 @@ var ACTIVE = "active", EXPANDED = "expanded", FOCUS = "focus", SELECTED = "selected"; + +/* Recursively load lazy nodes + * @param {string} mode 'load', 'expand', false + */ +function _loadLazyNodes(tree, instData, keyList, mode, dfd) { + var i, key, l, node, + foundOne = false, + deferredList = [], + // lazyNodeList = [], + missingKeyList = []; //keyList.slice(0), + + keyList = keyList || []; + // expand = expand !== false; + dfd = dfd || $.Deferred(); + + for( i=0, l=keyList.length; i<l; i++ ) { + key = keyList[i]; + node = tree.getNodeByKey(key); + if( node ) { + if( mode && node.isUndefined() ) { + // lazyNodeList.push(node); + foundOne = true; + tree.debug("_loadLazyNodes: " + node + " is lazy: loading..."); + if( mode === "expand" ) { + deferredList.push(node.setExpanded()); + } else { + deferredList.push(node.load()); + } + } else { + tree.debug("_loadLazyNodes: " + node + " already loaded."); + node.setExpanded(); + // node.expanded = true; + // node.render(); + } + } else { + missingKeyList.push(key); + tree.debug("_loadLazyNodes: " + node + " was not yet found."); + } + } + + $.when.apply($, deferredList).always(function(){ + // All lazy-expands have finished + if( foundOne && missingKeyList.length > 0 ) { + // If we read new nodes from server, try to resolve yet-missing keys + _loadLazyNodes(tree, instData, missingKeyList, mode, dfd); + } else { + if( missingKeyList.length ) { + tree.warn("_loadLazyNodes: could not load those keys: ", missingKeyList); + for( i=0, l=missingKeyList.length; i<l; i++ ) { + key = keyList[i]; + instData._setKey(EXPANDED, keyList[i], false); + } + } + dfd.resolve(); + } + }); + return dfd; +} + + /** - * + * [ext-persist] Remove persistence cookies of the given type(s). * Called like * $("#tree").fancytree("getTree").clearCookies("active expanded focus selected"); * - * @lends Fancytree.prototype + * @alias Fancytree#clearCookies * @requires jquery.fancytree.persist.js */ $.ui.fancytree._FancytreeClass.prototype.clearCookies = function(types){ var inst = this.ext.persist, cookiePrefix = inst.cookiePrefix; types = types || "active expanded focus selected"; // TODO: optimize if(types.indexOf(ACTIVE) >= 0){ - $.cookie(cookiePrefix + ACTIVE, null); + // $.cookie(cookiePrefix + ACTIVE, null); + $.removeCookie(cookiePrefix + ACTIVE); } if(types.indexOf(EXPANDED) >= 0){ - $.cookie(cookiePrefix + EXPANDED, null); + // $.cookie(cookiePrefix + EXPANDED, null); + $.removeCookie(cookiePrefix + EXPANDED); } if(types.indexOf(FOCUS) >= 0){ - $.cookie(cookiePrefix + FOCUS, null); + // $.cookie(cookiePrefix + FOCUS, null); + $.removeCookie(cookiePrefix + FOCUS); } if(types.indexOf(SELECTED) >= 0){ - $.cookie(cookiePrefix + SELECTED, null); + // $.cookie(cookiePrefix + SELECTED, null); + $.removeCookie(cookiePrefix + SELECTED); } }; /** -* Return persistence information from cookies -* -* Called like -* $("#tree").fancytree("getTree").getPersistData(); -* -* @lends Fancytree.prototype -* @requires jquery.fancytree.persist.js -*/ + * [ext-persist] Return persistence information from cookies + * + * Called like + * $("#tree").fancytree("getTree").getPersistData(); + * + * @alias Fancytree#getPersistData + * @requires jquery.fancytree.persist.js + */ $.ui.fancytree._FancytreeClass.prototype.getPersistData = function(){ var inst = this.ext.persist, instOpts= this.options.persist, delim = instOpts.cookieDelimiter, res = {}; res[ACTIVE] = $.cookie(inst.cookiePrefix + ACTIVE); res[EXPANDED] = ($.cookie(inst.cookiePrefix + EXPANDED) || "").split(delim); res[SELECTED] = ($.cookie(inst.cookiePrefix + SELECTED) || "").split(delim); res[FOCUS] = $.cookie(inst.cookiePrefix + FOCUS); + return res; }; /* ***************************************************************************** * Extension code */ $.ui.fancytree.registerExtension({ name: "persist", - version: "0.0.1", + version: "0.2.0", // Default options for this extension. options: { // appendRequestInfo: false, cookieDelimiter: "~", cookiePrefix: undefined, // 'fancytree-<treeId>-' by default @@ -5012,10 +5745,11 @@ expires: "", path: "", domain: "", secure: false }, + expandLazy: false, // true: recursively expand and load lazy nodes overrideSource: false, // true: cookie takes precedence over `source` data attributes. types: "active expanded focus selected" }, /* Append `key` to a cookie. */ @@ -5053,72 +5787,84 @@ instData.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0; instData.storeSelected = instOpts.types.indexOf(SELECTED) >= 0; instData.storeFocus = instOpts.types.indexOf(FOCUS) >= 0; // Bind init-handler to apply cookie state - tree.$div.bind("fancytreeinit", function(e){ - var cookie, - keyList, - i, - prevFocus = $.cookie(instData.cookiePrefix + FOCUS), // record this before node.setActive() overrides it - node; + tree.$div.bind("fancytreeinit", function(event){ + var cookie, dfd, i, keyList, node, + prevFocus = $.cookie(instData.cookiePrefix + FOCUS); // record this before node.setActive() overrides it; tree.debug("COOKIE " + document.cookie); - if(instData.storeExpanded){ - cookie = $.cookie(instData.cookiePrefix + EXPANDED); - if(cookie){ - keyList = cookie.split(instOpts.cookieDelimiter); - for(i=0; i<keyList.length; i++){ - node = tree.getNodeByKey(keyList[i]); - if(node){ - if(node.expanded === undefined || instOpts.overrideSource && (node.expanded === false)){ -// node.setExpanded(); - node.expanded = true; - node.render(); + cookie = $.cookie(instData.cookiePrefix + EXPANDED); + keyList = cookie && cookie.split(instOpts.cookieDelimiter); + + if( instData.storeExpanded ) { + // Recursively load nested lazy nodes if expandLazy is 'expand' or 'load' + // Also remove expand-cookies for unmatched nodes + dfd = _loadLazyNodes(tree, instData, keyList, instOpts.expandLazy ? "expand" : false , null); + } else { + // nothing to do + dfd = new $.Deferred().resolve(); + } + + dfd.done(function(){ + // alert("persistent expand done"); + // if(instData.storeExpanded){ + // cookie = $.cookie(instData.cookiePrefix + EXPANDED); + // if(cookie){ + // keyList = cookie.split(instOpts.cookieDelimiter); + // for(i=0; i<keyList.length; i++){ + // node = tree.getNodeByKey(keyList[i]); + // if(node){ + // if(node.expanded === undefined || instOpts.overrideSource && (node.expanded === false)){ + // // node.setExpanded(); + // node.expanded = true; + // node.render(); + // } + // }else{ + // // node is no longer member of the tree: remove from cookie + // instData._setKey(EXPANDED, keyList[i], false); + // } + // } + // } + // } + if(instData.storeSelected){ + cookie = $.cookie(instData.cookiePrefix + SELECTED); + if(cookie){ + keyList = cookie.split(instOpts.cookieDelimiter); + for(i=0; i<keyList.length; i++){ + node = tree.getNodeByKey(keyList[i]); + if(node){ + if(node.selected === undefined || instOpts.overrideSource && (node.selected === false)){ + // node.setSelected(); + node.selected = true; + node.renderStatus(); + } + }else{ + // node is no longer member of the tree: remove from cookie also + instData._setKey(SELECTED, keyList[i], false); } - }else{ - // node is no longer member of the tree: remove from cookie - instData._setKey(EXPANDED, keyList[i], false); } } } - } - if(instData.storeSelected){ - cookie = $.cookie(instData.cookiePrefix + SELECTED); - if(cookie){ - keyList = cookie.split(instOpts.cookieDelimiter); - for(i=0; i<keyList.length; i++){ - node = tree.getNodeByKey(keyList[i]); + if(instData.storeActive){ + cookie = $.cookie(instData.cookiePrefix + ACTIVE); + if(cookie && (opts.persist.overrideSource || !tree.activeNode)){ + node = tree.getNodeByKey(cookie); if(node){ - if(node.selected === undefined || instOpts.overrideSource && (node.selected === false)){ -// node.setSelected(); - node.selected = true; - node.renderStatus(); - } - }else{ - // node is no longer member of the tree: remove from cookie also - instData._setKey(SELECTED, keyList[i], false); + node.setActive(); } } } - } - if(instData.storeActive){ - cookie = $.cookie(instData.cookiePrefix + ACTIVE); - if(cookie && (opts.persist.overrideSource || !tree.activeNode)){ - node = tree.getNodeByKey(cookie); + if(instData.storeFocus && prevFocus){ + node = tree.getNodeByKey(prevFocus); if(node){ - node.setActive(); + node.setFocus(); } } - } - if(instData.storeFocus && prevFocus){ - node = tree.getNodeByKey(prevFocus); - if(node){ - node.setFocus(); - } - } + }); }); // Init the tree this._super(ctx); }, // treeDestroy: function(ctx){ @@ -5126,10 +5872,11 @@ // }, nodeSetActive: function(ctx, flag, opts) { var instData = this._local, instOpts = this.options.persist; + flag = flag !== false; this._super(ctx, flag, opts); if(instData.storeActive){ $.cookie(instData.cookiePrefix + ACTIVE, this.activeNode ? this.activeNode.key : null, @@ -5139,10 +5886,11 @@ nodeSetExpanded: function(ctx, flag, opts) { var res, node = ctx.node, instData = this._local; + flag = flag !== false; res = this._super(ctx, flag, opts); if(instData.storeExpanded){ instData._setKey(EXPANDED, node.key, flag); } @@ -5153,19 +5901,20 @@ instOpts = this.options.persist; this._super(ctx); if(instData.storeFocus){ - $.cookie(this.cookiePrefix + FOCUS, + $.cookie(instData.cookiePrefix + FOCUS, this.focusNode ? this.focusNode.key : null, instOpts.cookie); } }, nodeSetSelected: function(ctx, flag) { var node = ctx.node, instData = this._local; + flag = flag !== false; this._super(ctx, flag); if(instData.storeSelected){ instData._setKey(SELECTED, node.key, flag); } @@ -5182,12 +5931,12 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ ;(function($, window, document, undefined) { "use strict"; @@ -5208,12 +5957,13 @@ /* Show/hide all rows that are structural descendants of `parent`. */ function setChildRowVisibility(parent, flag) { parent.visit(function(node){ var tr = node.tr; + // currentFlag = node.hide ? false : flag; // fix for ext-filter if(tr){ - tr.style.display = flag ? "" : "none"; + tr.style.display = (node.hide || !flag) ? "none" : ""; } if(!node.expanded){ return "skip"; } }); @@ -5246,16 +5996,17 @@ } $.ui.fancytree.registerExtension({ name: "table", - version: "0.1.0", + version: "0.2.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 + checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) + customStatus: false, // true: generate renderColumns events for status nodes + indentation: 16, // indent every node level by 16px + nodeColumnIdx: 0 // render node expander, icon, and title to this column (default: #0) }, // Overide virtual methods for this extension. // `this` : is this extension object // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) treeInit: function(ctx){ @@ -5267,21 +6018,21 @@ tree.tbody = $table.find("> tbody")[0]; tree.columnCount = $("thead >tr >th", $table).length; $(tree.tbody).empty(); tree.rowFragment = document.createDocumentFragment(); - $row = $("<tr>"); + $row = $("<tr />"); tdRole = ""; if(ctx.options.aria){ $row.attr("role", "row"); tdRole = " role='gridcell'"; } for(i=0; i<tree.columnCount; i++) { if(ctx.options.table.nodeColumnIdx === i){ - $row.append("<td" + tdRole + "><span class='fancytree-node'></span></td>"); + $row.append("<td" + tdRole + "><span class='fancytree-node' /></td>"); }else{ - $row.append("<td" + tdRole + ">"); + $row.append("<td" + tdRole + " />"); } } tree.rowFragment.appendChild($row.get(0)); // Make sure that status classes are set on the node's <tr> elements @@ -5334,17 +6085,23 @@ isRootNode = !node.parent; if( !_recursive ){ ctx.hasCollapsedParents = node.parent && !node.parent.expanded; } - $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode); + // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); if( !isRootNode ){ if(!node.tr){ + if( ctx.hasCollapsedParents /*&& !node.parent.tr*/ ) { + // #166: we assume that the parent will be (recursively) rendered + // later anyway. + node.debug("nodeRender ignored due to unrendered parent"); + return; + } // Create new <tr> after previous row newRow = tree.rowFragment.firstChild.cloneNode(true); prevNode = findPrevRowNode(node); -// $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key); + // $.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"; }else if(deep && ctx.hasCollapsedParents){ @@ -5375,15 +6132,20 @@ // tree._triggerNodeEvent("createNode", ctx); 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); + if( force ) { + // Set icon, link, and title (normally this is only required on initial render) + this.nodeRenderTitle(ctx); // triggers renderColumns() + } else { + // Update element classes according to node state + this.nodeRenderStatus(ctx); + } } } - // Allow tweaking after node state was rendered + // Allow tweaking after node state was rendered // tree._triggerNodeEvent("renderNode", ctx); if ( opts.renderNode ){ opts.renderNode.call(tree, {type: "renderNode"}, ctx); } // Visit child nodes @@ -5416,28 +6178,34 @@ prevTr = n.tr; } }); } // Update element classes according to node state - if(!isRootNode){ - this.nodeRenderStatus(ctx); - } + // if(!isRootNode){ + // this.nodeRenderStatus(ctx); + // } }, nodeRenderTitle: function(ctx, title) { var $cb, node = ctx.node, opts = ctx.options; this._super(ctx); // Move checkbox to custom column - if(opts.checkbox && opts.table.checkboxColumnIdx != null){ + 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 // ctx.tree._triggerNodeEvent("renderColumns", node); - if ( opts.renderColumns ){ + // Update element classes according to node state + if( ! node.isRoot() ){ + this.nodeRenderStatus(ctx); + } + if( !opts.table.customStatus && node.isStatusNode() ) { + // default rendering for status node: leave other cells empty + } else if ( opts.renderColumns ) { opts.renderColumns.call(ctx.tree, {type: "renderColumns"}, ctx); } }, nodeRenderStatus: function(ctx) { var indent, @@ -5451,25 +6219,49 @@ indent = (node.getLevel() - 1) * opts.table.indentation; $(node.span).css({marginLeft: indent + "px"}); }, /* Expand node, return Deferred.promise. */ nodeSetExpanded: function(ctx, flag, opts) { - return this._super(ctx, flag, opts).always(function () { + var dfd = new $.Deferred(), + prevOpts = opts || {}; + + opts = $.extend({}, opts, {noEvents: true, noAnimation: true}); + + function _afterExpand(ok) { flag = (flag !== false); setChildRowVisibility(ctx.node, flag); + if( !prevOpts.noEvents ) { + ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); + } + if( ok ) { + dfd.resolveWith(ctx.node); + } else { + dfd.rejectWith(ctx.node); + } + } + this._super(ctx, flag, opts).done(function () { + _afterExpand(true); + }).fail(function () { + _afterExpand(false); }); + return dfd.promise(); }, nodeSetStatus: function(ctx, status, message, details) { if(status === "ok"){ var node = ctx.node, firstChild = ( node.children ? node.children[0] : null ); - if ( firstChild && firstChild.isStatusNode ) { + if ( firstChild && firstChild.isStatusNode() ) { $(firstChild.tr).remove(); } } this._super(ctx, status, message, details); - }/*, + }, + treeClear: function(ctx) { + this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); + return this._super(ctx); + } + /*, treeSetFocus: function(ctx, flag) { // alert("treeSetFocus" + ctx.tree.$container); ctx.tree.$container.focus(); $.ui.fancytree.focusTree = ctx.tree; }*/ @@ -5487,11 +6279,11 @@ * 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 + * @version 2.0.0-11 + * @date 2014-04-27T22:28 */ ;(function($, window, document, undefined) { "use strict";