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