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 = {"&": "&", "<": "<", ">": ">", "\"": """, "'": "'", "/": "/"},
//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 `<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 `<li><span>..</span> .. </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, """) + "'" : "";
+ 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 <, >, &, ", ', / 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));