vendor/assets/javascripts/fancytree/jquery.fancytree.js in fancytree-rails-2.0.0.pre.6.pre.1 vs vendor/assets/javascripts/fancytree/jquery.fancytree.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,41 +4007,21 @@ }); 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; - - -/* ***************************************************************************** - * Register AMD - */ -// 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; } ); -// } - -// 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));