webgme
Version:
Web-based Generic Modeling Environment
1,786 lines (1,717 loc) • 203 kB
JavaScript
/*!
* jquery.fancytree.js
* Tree view control with support for lazy loading and much more.
* https://github.com/mar10/fancytree/
*
* Copyright (c) 2008-2018, Martin Wendt (http://wwWendt.de)
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.30.1
* @date 2018-11-13T18:58:18Z
*/
/** Core Fancytree module.
*/
// UMD wrapper for the Fancytree core module
(function(factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery", "./jquery.fancytree.ui-deps"], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
require("./jquery.fancytree.ui-deps");
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function($) {
"use strict";
// prevent duplicate loading
if ($.ui && $.ui.fancytree) {
$.ui.fancytree.warn("Fancytree: ignored duplicate include");
return;
}
/******************************************************************************
* Private functions and variables
*/
var i,
attr,
FT = null, // initialized below
TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/'
REX_HTML = /[&<>"'\/]/g, // Escape those characters
REX_TOOLTIP = /[<>"'\/]/g, // Don't escape `&` in tooltips
RECURSIVE_REQUEST_ERROR = "$recursive_request",
ENTITY_MAP = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/",
},
IGNORE_KEYCODES = { 16: true, 17: true, 18: true },
SPECIAL_KEYCODES = {
8: "backspace",
9: "tab",
10: "return",
13: "return",
// 16: null, 17: null, 18: null, // ignore shift, ctrl, 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",
59: ";",
61: "=",
// 91: null, 93: null, // ignore left and right meta
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",
173: "-",
186: ";",
187: "=",
188: ",",
189: "-",
190: ".",
191: "/",
192: "`",
219: "[",
220: "\\",
221: "]",
222: "'",
},
MODIFIERS = {
16: "shift",
17: "ctrl",
18: "alt",
91: "meta",
93: "meta",
},
MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" },
// Boolean attributes that can be set with equivalent class names in the LI tags
// Note: v2.23: checkbox and hideCheckbox are *not* in this list
CLASS_ATTRS = "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split(
" "
),
CLASS_ATTR_MAP = {},
// Top-level Fancytree attributes, that can be set by dict
TREE_ATTRS = "columns types".split(" "),
// TREE_ATTR_MAP = {},
// Top-level FancytreeNode attributes, that can be set by dict
NODE_ATTRS = "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type unselectable unselectableIgnore unselectableStatus".split(
" "
),
NODE_ATTR_MAP = {},
// Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase)
NODE_ATTR_LOWERCASE_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;
}
for (i = 0; i < NODE_ATTRS.length; i++) {
attr = NODE_ATTRS[i];
NODE_ATTR_MAP[attr] = true;
if (attr !== attr.toLowerCase()) {
NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr;
}
}
// for(i=0; i<TREE_ATTRS.length; i++) {
// TREE_ATTR_MAP[TREE_ATTRS[i]] = true;
// }
function _assert(cond, msg) {
// TODO: see qunit.js extractStacktrace()
if (!cond) {
msg = msg ? ": " + msg : "";
// consoleApply("assert", [!!cond, msg]);
$.error("Fancytree assertion failed" + msg);
}
}
_assert($.ui, "Fancytree requires jQuery UI (http://jqueryui.com)");
function consoleApply(method, args) {
var i,
s,
fn = window.console ? window.console[method] : null;
if (fn) {
try {
fn.apply(window.console, args);
} catch (e) {
// IE 8?
s = "";
for (i = 0; i < args.length; i++) {
s += args[i];
}
fn(s);
}
}
}
/* support: IE8 Polyfil for Date.now() */
if (!Date.now) {
Date.now = function now() {
return new Date().getTime();
};
}
/*Return true if x is a FancytreeNode.*/
function _isNode(x) {
return !!(x.tree && x.statusNodeType !== undefined);
}
/** Return true if dotted version string is equal or higher than requested version.
*
* See http://jsfiddle.net/mar10/FjSAN/
*/
function isVersionAtLeast(dottedVersion, major, minor, patch) {
var i,
v,
t,
verParts = $.map($.trim(dottedVersion).split("."), function(e) {
return parseInt(e, 10);
}),
testParts = $.map(
Array.prototype.slice.call(arguments, 1),
function(e) {
return parseInt(e, 10);
}
);
for (i = 0; i < testParts.length; i++) {
v = verParts[i] || 0;
t = testParts[i] || 0;
if (v !== t) {
return v > t;
}
}
return true;
}
/**
* Deep-merge a list of objects (but replace array-type options).
*
* jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays.
* This variant is used to merge extension defaults with user options, and should
* merge objects, but override arrays (for example the `triggerStart: [...]` option
* of ext-edit). Also `null` values are copied over and not skipped.
*
* See issue #876
*
* Example:
* _simpleDeepMerge({}, o1, o2);
*/
function _simpleDeepMerge() {
var options,
name,
src,
copy,
clone,
target = arguments[0] || {},
i = 1,
length = arguments.length;
// Handle case when target is a string or something (possible in deep copy)
if (typeof target !== "object" && !$.isFunction(target)) {
target = {};
}
if (i === length) {
throw "need at least two args";
}
for (; i < length; i++) {
// Only deal with non-null/undefined values
if ((options = arguments[i]) != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects
// (NOTE: unlike $.extend, we don't merge arrays, but relace them)
if (copy && $.isPlainObject(copy)) {
clone = src && $.isPlainObject(src) ? src : {};
// Never move original objects, clone them
target[name] = _simpleDeepMerge(clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
}
/** Return a wrapper that calls sub.methodName() and exposes
* this : tree
* this._local : tree.ext.EXTNAME
* this._super : base.methodName.call()
* this._superApply : base.methodName.apply()
*/
function _makeVirtualFunction(methodName, tree, base, extension, extName) {
// $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName);
// if(rexTestSuper && !rexTestSuper.test(func)){
// // extension.methodName() doesn't call _super(), so no wrapper required
// return func;
// }
// Use an immediate function as closure
var proxy = (function() {
var prevFunc = tree[methodName], // org. tree method or prev. proxy
baseFunc = extension[methodName], //
_local = tree.ext[extName],
_super = function() {
return prevFunc.apply(tree, arguments);
},
_superApply = function(args) {
return prevFunc.apply(tree, args);
};
// Return the wrapper function
return function() {
var prevLocal = tree._local,
prevSuper = tree._super,
prevSuperApply = tree._superApply;
try {
tree._local = _local;
tree._super = _super;
tree._superApply = _superApply;
return baseFunc.apply(tree, arguments);
} finally {
tree._local = prevLocal;
tree._super = prevSuper;
tree._superApply = prevSuperApply;
}
};
})(); // end of Immediate Function
return proxy;
}
/**
* Subclass `base` by creating proxy functions
*/
function _subclassObject(tree, base, extension, extName) {
// $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName);
for (var attrName in extension) {
if (typeof extension[attrName] === "function") {
if (typeof tree[attrName] === "function") {
// override existing method
tree[attrName] = _makeVirtualFunction(
attrName,
tree,
base,
extension,
extName
);
} else if (attrName.charAt(0) === "_") {
// Create private methods in tree.ext.EXTENSION namespace
tree.ext[extName][attrName] = _makeVirtualFunction(
attrName,
tree,
base,
extension,
extName
);
} else {
$.error(
"Could not override tree." +
attrName +
". Use prefix '_' to create tree." +
extName +
"._" +
attrName
);
}
} else {
// Create member variables in tree.ext.EXTENSION namespace
if (attrName !== "options") {
tree.ext[extName][attrName] = extension[attrName];
}
}
}
}
function _getResolvedPromise(context, argArray) {
if (context === undefined) {
return $.Deferred(function() {
this.resolve();
}).promise();
} else {
return $.Deferred(function() {
this.resolveWith(context, argArray);
}).promise();
}
}
function _getRejectedPromise(context, argArray) {
if (context === undefined) {
return $.Deferred(function() {
this.reject();
}).promise();
} else {
return $.Deferred(function() {
this.rejectWith(context, argArray);
}).promise();
}
}
function _makeResolveFunc(deferred, context) {
return function() {
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 (old jQuery UI)
delete d.uiFancytree; // 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;
}
function _escapeTooltip(s) {
return ("" + s).replace(REX_TOOLTIP, function(s) {
return ENTITY_MAP[s];
});
}
// TODO: use currying
function _makeNodeTitleMatcher(s) {
s = s.toLowerCase();
return function(node) {
return node.title.toLowerCase().indexOf(s) >= 0;
};
}
function _makeNodeTitleStartMatcher(s) {
var reMatch = new RegExp("^" + s, "i");
return function(node) {
return reMatch.test(node.title);
};
}
/******************************************************************************
* FancytreeNode
*/
/**
* Creates a new FancytreeNode
*
* @class FancytreeNode
* @classdesc A FancytreeNode represents the hierarchical data model and operations.
*
* @param {FancytreeNode} parent
* @param {NodeData} obj
*
* @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 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 Additional CSS classes, added to the node's `<span>`.<br>
* Note: use `node.add/remove/toggleClass()` to modify.
* @property {boolean} folder Folder nodes have different default icons and click behavior.<br>
* Note: Also non-folders may have children.
* @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'.
* @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 popup
* @property {string} iconTooltip Description used as hover popup for icon. @since 2.27
* @property {string} type Node type, used with tree.types map. @since 2.27
*/
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.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 occurred
this.data = {};
// TODO: merge this code with node.toDict()
// copy attributes from obj object
for (i = 0, l = NODE_ATTRS.length; i < l; i++) {
name = NODE_ATTRS[i];
this[name] = obj[name];
}
// unselectableIgnore and unselectableStatus imply unselectable
if (
this.unselectableIgnore != null ||
this.unselectableStatus != null
) {
this.unselectable = true;
}
if (obj.hideCheckbox) {
$.error(
"'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'"
);
}
// node.data += obj.data
if (obj.data) {
$.extend(this.data, obj.data);
}
// Copy all other attributes to this.data.NAME
for (name in obj) {
if (
!NODE_ATTR_MAP[name] &&
!$.isFunction(obj[name]) &&
!NONE_NODE_DATA_MAP[name]
) {
// node.data.NAME = obj.NAME
this.data[name] = obj[name];
}
}
// Fix missing key
if (this.key == null) {
// test for null OR undefined
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++;
}
} else {
this.key = "" + this.key; // Convert to string (#217)
}
// 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
cl = obj.children;
if (cl) {
if (cl.length) {
this._setChildren(cl);
} else {
// if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded'
this.children = this.lazy ? [] : null;
}
} else {
this.children = null;
}
// 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. */
_findDirectChild: function(ptr) {
var i,
l,
cl = this.children;
if (cl) {
if (typeof ptr === "string") {
for (i = 0, l = cl.length; i < l; i++) {
if (cl[i].key === ptr) {
return cl[i];
}
}
} else if (typeof ptr === "number") {
return this.children[ptr];
} else if (ptr.parent === this) {
return ptr;
}
}
return null;
},
// TODO: activate()
// TODO: activateSilently()
/* Internal helper called in recursive addChildren sequence.*/
_setChildren: function(children) {
_assert(
children && (!this.children || this.children.length === 0),
"only init supported"
);
this.children = [];
for (var i = 0, l = children.length; i < l; i++) {
this.children.push(new FancytreeNode(this, children[i]));
}
},
/**
* 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).
* If omitted, the new children are appended.
* @returns {FancytreeNode} first child added
*
* @see FancytreeNode#applyPatch
*/
addChildren: function(children, insertBefore) {
var i,
l,
pos,
origFirstChild = this.getFirstChild(),
origLastChild = this.getLastChild(),
firstNode = null,
nodeList = [];
if ($.isPlainObject(children)) {
children = [children];
}
if (!this.children) {
this.children = [];
}
for (i = 0, l = children.length; i < l; i++) {
nodeList.push(new FancytreeNode(this, children[i]));
}
firstNode = nodeList[0];
if (insertBefore == null) {
this.children = this.children.concat(nodeList);
} else {
// Returns null if insertBefore is not a direct child:
insertBefore = this._findDirectChild(insertBefore);
pos = $.inArray(insertBefore, this.children);
_assert(pos >= 0, "insertBefore must be an existing child");
// insert nodeList after children[pos]
this.children.splice.apply(
this.children,
[pos, 0].concat(nodeList)
);
}
if (origFirstChild && !insertBefore) {
// #708: Fast path -- don't render every child of root, just the new ones!
// #723, #729: but only if it's appended to an existing child list
for (i = 0, l = nodeList.length; i < l; i++) {
nodeList[i].render(); // New nodes were never rendered before
}
// Adjust classes where status may have changed
// Has a first child
if (origFirstChild !== this.getFirstChild()) {
// Different first child -- recompute classes
origFirstChild.renderStatus();
}
if (origLastChild !== this.getLastChild()) {
// Different last child -- recompute classes
origLastChild.renderStatus();
}
} else if (!this.parent || this.parent.ul || this.tr) {
// render if the parent was rendered (or this is a root node)
this.render();
}
if (this.tree.options.selectMode === 3) {
this.fixSelection3FromEndNodes();
}
this.triggerModifyChild(
"add",
nodeList.length === 1 ? nodeList[0] : null
);
return firstNode;
},
/**
* Add class to node's span tag and to .extraClasses.
*
* @param {string} className class name
*
* @since 2.17
*/
addClass: function(className) {
return this.toggleClass(className, true);
},
/**
* 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=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child')
* @returns {FancytreeNode} new node
*/
addNode: function(node, mode) {
if (mode === undefined || mode === "over") {
mode = "child";
}
switch (mode) {
case "after":
return this.getParent().addChildren(
node,
this.getNextSibling()
);
case "before":
return this.getParent().addChildren(node, this);
case "firstChild":
// Insert before the first child if any
var insertBefore = this.children ? this.children[0] : null;
return this.addChildren(node, insertBefore);
case "child":
case "over":
return this.addChildren(node);
}
_assert(false, "Invalid mode: " + mode);
},
/**Add child status nodes that indicate 'More...', etc.
*
* This also maintains the node's `partload` property.
* @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes.
* @param {string} [mode='child'] 'child'|firstChild'
* @since 2.15
*/
addPagingNode: function(node, mode) {
var i, n;
mode = mode || "child";
if (node === false) {
for (i = this.children.length - 1; i >= 0; i--) {
n = this.children[i];
if (n.statusNodeType === "paging") {
this.removeChild(n);
}
}
this.partload = false;
return;
}
node = $.extend(
{
title: this.tree.options.strings.moreData,
statusNodeType: "paging",
icon: false,
},
node
);
this.partload = true;
return this.addNode(node, 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
*/
applyPatch: function(patch) {
// patch [key, null] means 'remove'
if (patch === null) {
this.remove();
return _getResolvedPromise(this);
}
// TODO: make sure that root node is not collapsed or modified
// copy (most) attributes to node.ATTR or node.data.ATTR
var name,
promise,
v,
IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global
for (name in patch) {
v = patch[name];
if (!IGNORE_MAP[name] && !$.isFunction(v)) {
if (NODE_ATTR_MAP[name]) {
this[name] = v;
} else {
this.data[name] = v;
}
}
}
// Remove and/or create children
if (patch.hasOwnProperty("children")) {
this.removeChildren();
if (patch.children) {
// only if not null and not empty list
// TODO: addChildren instead?
this._setChildren(patch.children);
}
// TODO: how can we APPEND or INSERT child nodes?
}
if (this.isVisible()) {
this.renderTitle();
this.renderStatus();
}
// Expand collapse (final step, since this may be async)
if (patch.hasOwnProperty("expanded")) {
promise = this.setExpanded(patch.expanded);
} else {
promise = _getResolvedPromise(this);
}
return promise;
},
/** Collapse all sibling nodes.
* @returns {$.Promise}
*/
collapseSiblings: function() {
return this.tree._callHook("nodeCollapseSiblings", this);
},
/** Copy this node as sibling or child of `node`.
*
* @param {FancytreeNode} node source node
* @param {string} [mode=child] '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
* @returns {int} number of child nodes
*/
countChildren: function(deep) {
var cl = this.children,
i,
l,
n;
if (!cl) {
return 0;
}
n = cl.length;
if (deep !== false) {
for (i = 0, l = n; i < l; i++) {
n += cl[i].countChildren();
}
}
return n;
},
// TODO: deactivate()
/** Write to browser console if debugLevel >= 4 (prepending node info)
*
* @param {*} msg string or object or array of such
*/
debug: function(msg) {
if (this.tree.options.debugLevel >= 4) {
Array.prototype.unshift.call(arguments, this.toString());
consoleApply("log", arguments);
}
},
/** Deprecated.
* @deprecated since 2014-02-16. Use resetLazy() instead.
*/
discard: function() {
this.warn(
"FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead."
);
return this.resetLazy();
},
/** Remove DOM elements for all descendents. May be called on .collapse event
* to keep the DOM small.
* @param {boolean} [includeSelf=false]
*/
discardMarkup: function(includeSelf) {
var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup";
this.tree._callHook(fn, this);
},
/** Write error to browser console if debugLevel >= 1 (prepending tree info)
*
* @param {*} msg string or object or array of such
*/
error: function(msg) {
if (this.options.debugLevel >= 1) {
Array.prototype.unshift.call(arguments, this.toString());
consoleApply("error", arguments);
}
},
/**Find all nodes that match condition (excluding self).
*
* @param {string | function(node)} match title string to search for, or a
* callback function that returns `true` if a node is matched.
* @returns {FancytreeNode[]} array of nodes (may be empty)
*/
findAll: function(match) {
match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
var res = [];
this.visit(function(n) {
if (match(n)) {
res.push(n);
}
});
return res;
},
/**Find first node that matches condition (excluding self).
*
* @param {string | function(node)} match title string to search for, or a
* callback function that returns `true` if a node is matched.
* @returns {FancytreeNode} matching node or null
* @see FancytreeNode#findAll
*/
findFirst: function(match) {
match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
var res = null;
this.visit(function(n) {
if (match(n)) {
res = n;
return false;
}
});
return res;
},
/* Apply selection state (internal use only) */
_changeSelectStatusAttrs: function(state) {
var changed = false,
opts = this.tree.options,
unselectable = FT.evalOption(
"unselectable",
this,
this,
opts,
false
),
unselectableStatus = FT.evalOption(
"unselectableStatus",
this,
this,
opts,
undefined
);
if (unselectable && unselectableStatus != null) {
state = unselectableStatus;
}
switch (state) {
case false:
changed = this.selected || this.partsel;
this.selected = false;
this.partsel = false;
break;
case true:
changed = !this.selected || !this.partsel;
this.selected = true;
this.partsel = true;
break;
case undefined:
changed = this.selected || !this.partsel;
this.selected = false;
this.partsel = true;
break;
default:
_assert(false, "invalid state: " + state);
}
// this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed);
if (changed) {
this.renderStatus();
}
return changed;
},
/**
* Fix selection status, after this node was (de)selected in multi-hier mode.
* This includes (de)selecting all children.
*/
fixSelection3AfterClick: function(callOpts) {
var flag = this.isSelected();
// this.debug("fixSelection3AfterClick()");
this.visit(function(node) {
node._changeSelectStatusAttrs(flag);
});
this.fixSelection3FromEndNodes(callOpts);
},
/**
* Fix selection status for multi-hier mode.
* Only end-nodes are considered to update the descendants branch and parents.
* Should be called after this node has loaded new children or after
* children have been modified using the API.
*/
fixSelection3FromEndNodes: function(callOpts) {
var opts = this.tree.options;
// this.debug("fixSelection3FromEndNodes()");
_assert(opts.selectMode === 3, "expected selectMode 3");
// Visit all end nodes and adjust their parent's `selected` and `partsel`
// attributes. Return selection state true, false, or undefined.
function _walk(node) {
var i,
l,
child,
s,
state,
allSelected,
someSelected,
unselIgnore,
unselState,
children = node.children;
if (children && children.length) {
// check all children recursively
allSelected = true;
someSelected = false;
for (i = 0, l = children.length; i < l; i++) {
child = children[i];
// the selection state of a node is not relevant; we need the end-nodes
s = _walk(child);
// if( !child.unselectableIgnore ) {
unselIgnore = FT.evalOption(
"unselectableIgnore",
child,
child,
opts,
false
);
if (!unselIgnore) {
if (s !== false) {
someSelected = true;
}
if (s !== true) {
allSelected = false;
}
}
}
state = allSelected
? true
: someSelected
? undefined
: false;
} else {
// This is an end-node: simply report the status
unselState = FT.evalOption(
"unselectableStatus",
node,
node,
opts,
undefined
);
state = unselState == null ? !!node.selected : !!unselState;
}
node._changeSelectStatusAttrs(state);
return state;
}
_walk(this);
// Update parent's state
this.visitParents(function(node) {
var i,
l,
child,
state,
unselIgnore,
unselState,
children = node.children,
allSelected = true,
someSelected = false;
for (i = 0, l = children.length; i < l; i++) {
child = children[i];
unselIgnore = FT.evalOption(
"unselectableIgnore",
child,
child,
opts,
false
);
if (!unselIgnore) {
unselState = FT.evalOption(
"unselectableStatus",
child,
child,
opts,
undefined
);
state =
unselState == null
? !!child.selected
: !!unselState;
// When fixing the parents, we trust the sibling status (i.e.
// we don't recurse)
if (state || child.partsel) {
someSelected = true;
}
if (!state) {
allSelected = false;
}
}
}
state = allSelected ? true : someSelected ? undefined : false;
node._changeSelectStatusAttrs(state);
});
},
// TODO: focus()
/**
* Update node data. If dict contains 'children', then also replace
* the hole sub tree.
* @param {NodeData} dict
*
* @see FancytreeNode#addChildren
* @see FancytreeNode#applyPatch
*/
fromDict: function(dict) {
// copy all other attributes to this.data.xxx
for (var name in dict) {
if (NODE_ATTR_MAP[name]) {
// node.NAME = dict.NAME
this[name] = dict[name];
} else if (name === "data") {
// node.data += dict.data
$.extend(this.data, dict.data);
} else if (
!$.isFunction(dict[name]) &&
!NONE_NODE_DATA_MAP[name]
) {
// node.data.NAME = dict.NAME
this.data[name] = dict[name];
}
}
if (dict.children) {
// recursively set children and render
this.removeChildren();
this.addChildren(dict.children);
}
this.renderTitle();
/*
var children = dict.children;
if(children === undefined){
this.data = $.extend(this.data, dict);
this.render();
return;
}
dict = $.extend({}, dict);
dict.children = undefined;
this.data = $.extend(this.data, dict);
this.removeChildren();
this.addChild(children);
*/
},
/** 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;
},
/** Return the first child node or null.
* @returns {FancytreeNode | null}
*/
getFirstChild: function() {
return this.children ? this.children[0] : null;
},
/** 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
},
/** Return the hierarchical child index (1-based, e.g. '3.2.4').
* @param {string} [separator="."]
* @param {int} [digits=1]
* @returns {string}
*/
getIndexHier: function(separator, digits) {
separator = separator || ".";
var s,
res = [];
$.each(this.getParentList(false, true), function(i, o) {
s = "" + (o.getIndex() + 1);
if (digits) {
// prepend leading zeroes
s = ("0000000" + s).substr(-digits);
}
res.push(s);
});
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}
*/
getKeyPath: function(excludeSelf) {
var path = [],
sep = this.tree.options.keyPathSeparator;
this.visitParents(function(n) {
if (n.parent) {
path.unshift(n.key);
}
}, !excludeSelf);
return sep + path.join(sep);
},
/** Return the last child of this node or null.
* @returns {FancytreeNode | null}
*/
getLastChild: function() {
return this.children
? this.children[this.children.length - 1]
: null;
},
/** 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;
},
/** 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;
for (i = 0, l = ac.length - 1; i < l; i++) {
// up to length-2, so next(last) = null
if (ac[i] === this) {
return ac[i + 1];
}
}
}
return null;
},
/** 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;
},
/** 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;
while (dtn) {
if (includeRoot || dtn.parent) {
l.unshift(dtn);
}
dtn = dtn.parent;
}
return l;
},
/** 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;
for (i = 1, l = ac.length; i < l; i++) {
// start with 1, so prev(first) = null
if (ac[i] === this) {
return ac[i - 1];
}
}
}
return null;
},
/**
* Return an array of selected descendant nodes.
* @param {boolean} [stopOnParents=false] only return the topmost selected
* node (useful with selectMode 3)
* @returns {FancytreeNode[]}
*/
getSelectedNodes: function(stopOnParents) {
var nodeList = [];
this.visit(function(node) {
if (node.selected) {
nodeList.push(node);
if (stopOnParents === true) {
return "skip"; // stop processing this branch
}
}
});
return nodeList;
},
/** 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()
) {
// Currently loading or load error
return undefined;
}
return true;
}
return !!(this.children && this.children.length);
},
/** Return true if node has keyboard focus.
* @returns {boolean}
*/
hasFocus: function() {
return this.tree.hasFocus() && this.tree.focusNode === this;
},
/** Write to browser console if debugLevel >= 3 (prepending node info)
*
* @param {*} msg string or object or array of such
*/
info: function(msg) {
if (this.tree.options.debugLevel >= 3) {
Array.prototype.unshift.call(arguments, this.toString());
consoleApply("info", arguments);
}
},
/** Return true if node is active (see also FancytreeNode#isSelected).
* @returns {boolean}
*/
isActive: function() {
return this.tree.activeNode === this;
},
/** Return true if node is vertically below `otherNode`, i.e. rendered in a subsequent row.
* @param {FancytreeNode} otherNode
* @returns {boolean}
* @since 2.28
*/
isBelowOf: function(otherNode) {
return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5);
},
/** Return true if node is a direct child of otherNode.
* @param {FancytreeNode} 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}
*/
isDescendantOf: function(otherNode) {
if (!otherNode || otherNode.tree !== this.tree) {
return false;
}
var p = this.parent;
while (p) {
if (p === otherNode) {
return true;
}
if (p === p.parent) {
$.error("Recursive parent link: " + p);
}
p = p.parent;
}
return false;
},
/** Return true if node is expanded.
* @returns {boolean}
*/
isExpanded: function() {
return !!this.expanded;
},
/** 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;
},
/** Return true if node is a folder, i.e. has the node.folder attribute set.
* @returns {boolean}
*/
isFolder: function() {
return !!this.folder;
},
/** 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;
},
/** Return true if node is lazy (even if data was already loaded)
* @returns {boolean}
*/
isLazy: function() {
return !!this.lazy;
},
/** 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() {
return !!this._isLoading;
},
/*
* @deprecated since v2.4.0: Use isRootNode() instead
*/
isRoot: function() {
return this.isRootNode();
},
/** Return true if node is partially selected (tri-state).
* @returns {boolean}
* @since 2.23
*/
isPartsel: function() {
return !this.selected && !!this.partsel;
},
/** (experimental) Return true if this is partially loaded.
* @returns {boolean}
* @since 2.15
*/
isPartload: function() {
return !!this.partload;
},
/** Return true if this is the (invisible) system root node.
* @returns {boolean}
* @since 2.4
*/
isRootNode: function() {
return this.tree.rootNode === this;
},
/** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive).
* @returns {boolean}
*/
isSelected: function() {
return !!this.selected;
},
/** Return true if this node is a temporarily generated system node like
* 'loading', 'paging', or 'error' (node.statusNodeType contains the type).
* @returns {boolean}
*/
isStatusNode: function() {
return !!this.statusNodeType;
},
/** Return true if this node is a status node of type 'paging'.
* @returns {boolean}
* @since 2.15
*/
isPagingNode: function() {
return this.statusNodeType === "paging";
},
/** Return true if this a top level node, i.e. a direct child of the (invisible) system root node.
* @returns {boolean}
* @since 2.4
*/
isTopLevel: function() {
return this.tree.rootNode === this.parent;
},
/** 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;
},
/** Deprecated.
* @deprecated since 2014-02-16: use load() instead.
*/
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 if neccessary. The <i>expanded</i> state is maintained.
* @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded.
* @returns {$.Promise}
*/
load: function(forceReload) {
var res,
source,
that = this,
wasExpanded = this.isExpanded();
_assert(this.isLazy(), "load() requires a lazy node");
// _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" );
if (!forceReload && !this.isUndefined()) {
return _getResolvedPromise(this);
}
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 (wasExpanded) {
this.expanded = true;
res.always(function() {
that.render();
});
} else {
res.always(function() {
that.renderStatus(); // fix expander icon to 'loaded'
});
}
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 <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'.
* 'firstChild': add this node as first child of targetNode.
* 'before': add this node as sibling before targetNode.
* '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";
} else if (mode === "firstChild") {
if (targetNode.children && targetNode.children.length) {
mode = "before";
targetNode = targetNode.children[0];
} else {
mode = "child";
}
}
var pos,
prevParent = this.parent,
targetParent =
mode === "child" ? targetNode : targetNode.parent;
if (this === targetNode) {
return;
} else if (!this.parent) {
$.error("Cannot move system root");
} else if (targetParent.isDescendantOf(this)) {
$.error("Cannot move a node to its own descendant");
}
if (targetParent !== prevParent) {
prevParent.triggerModifyChild("remove", this);
}
// Unlink this node from current parent
if (this.parent.children.length === 1) {
if (this.parent === targetParent) {
return; // #258
}
this.parent.children = this.parent.lazy ? [] : null;
this.parent.expanded = false;
} else {
pos = $.inArray(this, this.parent.children);
_assert(pos >= 0, "invalid source parent");
this.parent.children.splice(pos, 1);
}
// Remove from source DOM parent
// if(this.parent.ul){
// this.parent.ul.removeChild(this.li);
// }
// Insert this node to target parent's child list
this.parent = targetParent;
if (targetParent.hasChildren()) {
switch (mode) {
case "child":
// Append to existing target children
targetParent.children.push(this);
break;
case "before":
// Insert this node before target node
pos = $.inArray(targetNode, targetParent.children);
_assert(pos >= 0, "invalid target parent");
targetParent.children.splice(pos, 0, this);
break;
case "after":
// Insert this node after target node
pos = $.inArray(targetNode, targetParent.children);
_assert(pos >= 0, "invalid target parent");
targetParent.children.splice(pos + 1, 0, this);
break;
default:
$.error("Invalid mode " + mode);
}
} else {
targetParent.children = [this];
}
// Parent has no <ul> tag yet:
// if( !targetParent.ul ) {
// // This is the parent's first child: create UL tag
// // (Hidden, because it will be
// targetParent.ul = document.createElement("ul");
// targetParent.ul.style.display = "none";
// targetParent.li.appendChild(targetParent.ul);
// }
// // Issue 319: Add to target DOM parent (only if node was already rendered(expanded))
// if(this.li){
// targetParent.ul.appendChild(this.li);
// }^
// Let caller modify the nodes
if (map) {
targetNode.visit(map, true);
}
if (targetParent === prevParent) {
targetParent.triggerModifyChild("move", this);
} else {
// prevParent.triggerModifyChild("remove", this);
targetParent.triggerModifyChild("add", this);
}
// Handle cross-tree moves
if (this.tree !== targetNode.tree) {
// Fix node.tree for all source nodes
// _assert(false, "Cross-tree move is not yet implemented.");
this.warn("Cross-tree moveTo is experimantal!");
this.visit(function(n) {
// 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);
// }
// Update HTML markup
if (!prevParent.isDescendantOf(targetParent)) {
prevParent.render();
}
if (
!targetParent.isDescendantOf(prevParent) &&
targetParent !== prevParent
) {
targetParent.render();
}
// TODO: fix selection state
// TODO: fix active state
/*
var tree = this.tree;
var opts = tree.options;
var pers = tree.persistence;
// Always expand, if it's below minExpandLevel
// tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel());
if ( opts.minExpandLevel >= ftnode.getLevel() ) {
// tree.logDebug ("Force expand for %o", ftnode);
this.bExpanded = true;
}
// In multi-hier mode, update the parents selection state
// 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;
while( p ) {
if( !p.hasSubSel )
p._setSubSel(true);
p = p.parent;
}
}
// render this node and the new child
if ( tree.bEnableUpdate )
this.render();
return ftnode;
*/
},
/** Set focus relative to this node and optionally activate.
*
* @param {number} where The keyCode that would normally trigger this move,
* e.g. `$.ui.keyCode.LEFT` would collapse the node if it
* is expanded or move to the parent oterwise.
* @param {boolean} [activate=true]
* @returns {$.Promise}
*/
navigate: function(where, activate) {
var i,
parents,
res,
handled = true,
KC = $.ui.keyCode,
sib = null;
// Navigate to node
function _goto(n) {
if (n) {
// setFocus/setActive will scroll later (if autoScroll is specified)
try {