mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
6986 lines
287 KiB
JavaScript
6986 lines
287 KiB
JavaScript
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* The aciTree low-level DOM functions.
|
|
*
|
|
* A collection of functions optimised for aciTree DOM structure.
|
|
*
|
|
* Need to be included before the aciTree core and after aciPlugin.
|
|
*/
|
|
|
|
aciPluginClass.plugins.aciTree_dom = {
|
|
// get the UL container from a LI
|
|
// `node` must be valid LI DOM node
|
|
// can return NULL
|
|
container: function(node) {
|
|
var container = node.lastChild;
|
|
if (container && (container.nodeName == 'UL')) {
|
|
return container;
|
|
}
|
|
return null;
|
|
},
|
|
// get the first children from a LI (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node
|
|
// can return NULL
|
|
firstChild: function(node, callback) {
|
|
var container = this.container(node);
|
|
if (container) {
|
|
var firstChild = container.firstChild;
|
|
if (callback) {
|
|
while (firstChild && !callback.call(this, firstChild)) {
|
|
firstChild = firstChild.nextSibling;
|
|
}
|
|
}
|
|
return firstChild;
|
|
}
|
|
return null;
|
|
},
|
|
// get the last children from a LI (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node
|
|
// can return NULL
|
|
lastChild: function(node, callback) {
|
|
var container = this.container(node);
|
|
if (container) {
|
|
var lastChild = container.lastChild;
|
|
if (callback) {
|
|
while (lastChild && !callback.call(this, lastChild)) {
|
|
lastChild = lastChild.previousSibling;
|
|
}
|
|
}
|
|
return lastChild;
|
|
}
|
|
return null;
|
|
},
|
|
// get the previous LI sibling (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node
|
|
// can return NULL
|
|
prev: function(node, callback) {
|
|
var previous = node.previousSibling;
|
|
if (callback) {
|
|
while (previous && !callback.call(this, previous)) {
|
|
previous = previous.previousSibling;
|
|
}
|
|
}
|
|
return previous;
|
|
},
|
|
// get the next LI sibling (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node
|
|
// can return NULL
|
|
next: function(node, callback) {
|
|
var next = node.nextSibling;
|
|
if (callback) {
|
|
while (next && !callback.call(this, next)) {
|
|
next = next.nextSibling;
|
|
}
|
|
}
|
|
return next;
|
|
},
|
|
// get the previous LI in tree order (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node or NULL to prevent drill down/skip the node
|
|
// can return NULL
|
|
prevAll: function(node, callback) {
|
|
var previous, lastChild, drillDown, match, prev, parent;
|
|
while (true) {
|
|
previous = this.prev(node);
|
|
if (previous) {
|
|
if (callback) {
|
|
match = callback.call(this, previous);
|
|
if (match === null) {
|
|
node = previous;
|
|
continue;
|
|
}
|
|
}
|
|
lastChild = this.lastChild(previous);
|
|
if (lastChild) {
|
|
if (callback && (callback.call(this, lastChild) === null)) {
|
|
node = lastChild;
|
|
continue;
|
|
}
|
|
prev = false;
|
|
while (drillDown = this.lastChild(lastChild)) {
|
|
lastChild = drillDown;
|
|
if (callback) {
|
|
match = callback.call(this, lastChild);
|
|
if (match === null) {
|
|
node = lastChild;
|
|
prev = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (prev) {
|
|
continue;
|
|
}
|
|
if (callback) {
|
|
match = callback.call(this, lastChild);
|
|
if (match) {
|
|
return lastChild;
|
|
} else if (match !== null) {
|
|
node = lastChild;
|
|
continue;
|
|
}
|
|
} else {
|
|
return lastChild;
|
|
}
|
|
} else {
|
|
if (!callback || match) {
|
|
return previous;
|
|
} else {
|
|
node = previous;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
parent = this.parent(node);
|
|
if (parent) {
|
|
if (callback) {
|
|
match = callback.call(this, parent);
|
|
if (match) {
|
|
return parent;
|
|
} else {
|
|
node = parent;
|
|
}
|
|
} else {
|
|
return parent;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// get the next LI in tree order (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node or NULL to prevent drill down/skip the node
|
|
// can return NULL
|
|
nextAll: function(node, callback) {
|
|
var firstChild, match, next, parent, child;
|
|
while (true) {
|
|
firstChild = this.firstChild(node);
|
|
if (firstChild) {
|
|
if (callback) {
|
|
match = callback.call(this, firstChild);
|
|
if (match) {
|
|
return firstChild;
|
|
} else {
|
|
node = firstChild;
|
|
if (match !== null) {
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
return firstChild;
|
|
}
|
|
}
|
|
while (true) {
|
|
next = this.next(node);
|
|
if (next) {
|
|
if (callback) {
|
|
match = callback.call(this, next);
|
|
if (match) {
|
|
return next;
|
|
} else {
|
|
node = next;
|
|
if (match !== null) {
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
return next;
|
|
}
|
|
} else {
|
|
parent = node;
|
|
child = null;
|
|
while (parent = this.parent(parent)) {
|
|
next = this.next(parent);
|
|
if (next) {
|
|
if (callback) {
|
|
match = callback.call(this, next);
|
|
if (match) {
|
|
return next;
|
|
} else {
|
|
node = next;
|
|
if (match !== null) {
|
|
child = true;
|
|
} else {
|
|
child = false;
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
return next;
|
|
}
|
|
}
|
|
}
|
|
if (child !== null) {
|
|
if (child) {
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// get the first LI in tree order (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node or NULL to prevent drill down/skip the node
|
|
// can return NULL
|
|
first: function(node, callback) {
|
|
var container = this.container(node);
|
|
if (container) {
|
|
var firstChild = container.firstChild;
|
|
if (firstChild) {
|
|
if (callback && !callback.call(this, firstChild)) {
|
|
return this.nextAll(firstChild, callback);
|
|
}
|
|
return firstChild;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// get the last LI in tree order (with filtering)
|
|
// `node` must be valid LI DOM node
|
|
// `callback` can return FALSE to skip a node or NULL to prevent drill down/skip the node
|
|
// can return NULL
|
|
last: function(node, callback) {
|
|
var container = this.container(node);
|
|
if (container) {
|
|
var lastChild = container.lastChild;
|
|
if (lastChild) {
|
|
if (callback && (callback.call(this, lastChild) === null)) {
|
|
return this.prevAll(lastChild, callback);
|
|
} else {
|
|
var drillDown;
|
|
while (drillDown = this.lastChild(lastChild)) {
|
|
lastChild = drillDown;
|
|
}
|
|
if (callback && !callback.call(this, lastChild)) {
|
|
return this.prevAll(lastChild, callback);
|
|
}
|
|
return lastChild;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// get the children LI from the node
|
|
// `node` must be valid LI DOM node
|
|
// `drillDown` if TRUE all children are returned
|
|
// `callback` can return FALSE to skip a node or NULL to prevent drill down/skip the node
|
|
children: function(node, drillDown, callback) {
|
|
var children = [], levels = [], match, next, skip;
|
|
var firstChild = this.firstChild(node);
|
|
if (firstChild) {
|
|
while (true) {
|
|
skip = false;
|
|
do {
|
|
if (callback) {
|
|
match = callback.call(this, firstChild);
|
|
if (match) {
|
|
children.push(firstChild);
|
|
}
|
|
if (drillDown && (match !== null)) {
|
|
next = this.firstChild(firstChild);
|
|
if (next) {
|
|
levels.push(firstChild);
|
|
firstChild = next;
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
children.push(firstChild);
|
|
if (drillDown) {
|
|
next = this.firstChild(firstChild);
|
|
if (next) {
|
|
levels.push(firstChild);
|
|
firstChild = next;
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} while (firstChild = firstChild.nextSibling);
|
|
if (!skip) {
|
|
while (firstChild = levels.pop()) {
|
|
firstChild = firstChild.nextSibling;
|
|
if (firstChild) {
|
|
break;
|
|
}
|
|
}
|
|
if (!firstChild) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return children;
|
|
},
|
|
// get a children from the node
|
|
// `node` must be valid DOM node
|
|
// `callback` can return FALSE to skip a node or NULL to stop the search
|
|
// can return NULL
|
|
childrenTill: function(node, callback) {
|
|
var levels = [], match, next, skip;
|
|
var firstChild = node.firstChild;
|
|
if (firstChild) {
|
|
while (true) {
|
|
skip = false;
|
|
do {
|
|
match = callback.call(this, firstChild);
|
|
if (match) {
|
|
return firstChild;
|
|
} else if (match === null) {
|
|
return null;
|
|
}
|
|
next = firstChild.firstChild;
|
|
if (next) {
|
|
levels.push(firstChild);
|
|
firstChild = next;
|
|
skip = true;
|
|
break;
|
|
}
|
|
} while (firstChild = firstChild.nextSibling);
|
|
if (!skip) {
|
|
while (firstChild = levels.pop()) {
|
|
firstChild = firstChild.nextSibling;
|
|
if (firstChild) {
|
|
break;
|
|
}
|
|
}
|
|
if (!firstChild) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// get a children from the node having a class
|
|
// `node` must be valid DOM node
|
|
// `className` String or Array to check for
|
|
// can return NULL
|
|
childrenByClass: function(node, className) {
|
|
if (node.getElementsByClassName) {
|
|
var list = node.getElementsByClassName(className instanceof Array ? className.join(' ') : className);
|
|
return list ? list[0] : null;
|
|
} else {
|
|
return this.childrenTill(node, function(node) {
|
|
return this.hasClass(node, className);
|
|
});
|
|
}
|
|
},
|
|
// get the parent LI from the children LI
|
|
// `node` must be valid LI DOM node
|
|
// can return NULL
|
|
parent: function(node) {
|
|
var parent = node.parentNode.parentNode;
|
|
if (parent && (parent.nodeName == 'LI')) {
|
|
return parent;
|
|
}
|
|
return null;
|
|
},
|
|
// get the parent LI from any children
|
|
// `node` must be valid children of a LI DOM node
|
|
// can return NULL
|
|
parentFrom: function(node) {
|
|
while (node.nodeName != 'LI') {
|
|
node = node.parentNode;
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
}
|
|
return node;
|
|
},
|
|
// get a parent from the node
|
|
// `node` must be valid DOM node
|
|
// `callback` can return FALSE to skip a node or NULL to stop the search
|
|
// can return NULL
|
|
parentTill: function(node, callback) {
|
|
var match;
|
|
while (node = node.parentNode) {
|
|
match = callback.call(this, node);
|
|
if (match) {
|
|
return node;
|
|
} else if (match === null) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// get a parent from the node having a class
|
|
// `node` must be valid DOM node
|
|
// `className` String or Array to check for
|
|
// can return NULL
|
|
parentByClass: function(node, className) {
|
|
return this.parentTill(node, function(node) {
|
|
return this.hasClass(node, className);
|
|
});
|
|
},
|
|
// test if node has class(es)
|
|
// `className` String or Array to check for
|
|
// `withOut` String or Array to exclude with
|
|
hasClass: function(node, className, withOut) {
|
|
var oldClass = ' ' + node.className + ' ';
|
|
if (withOut instanceof Array) {
|
|
for (var i = 0; i < withOut.length; i++) {
|
|
if (oldClass.indexOf(' ' + withOut[i] + ' ') != -1) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
if (withOut && oldClass.indexOf(' ' + withOut + ' ') != -1) {
|
|
return false;
|
|
}
|
|
}
|
|
if (className instanceof Array) {
|
|
for (var i = 0; i < className.length; i++) {
|
|
if (oldClass.indexOf(' ' + className[i] + ' ') == -1) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
if (className && oldClass.indexOf(' ' + className + ' ') == -1) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
// filter nodes with class(es)
|
|
// `nodes` Array of DOM nodes
|
|
// @see `hasClass`
|
|
withClass: function(nodes, className, withOut) {
|
|
var filter = [];
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
if (this.hasClass(nodes[i], className, withOut)) {
|
|
filter.push(nodes[i]);
|
|
}
|
|
}
|
|
return filter;
|
|
},
|
|
// test if node has any class(es)
|
|
// `className` String or Array to check for (any class)
|
|
// `withOut` String or Array to exclude with
|
|
hasAnyClass: function(node, className, withOut) {
|
|
var oldClass = ' ' + node.className + ' ';
|
|
if (withOut instanceof Array) {
|
|
for (var i = 0; i < withOut.length; i++) {
|
|
if (oldClass.indexOf(' ' + withOut[i] + ' ') != -1) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
if (withOut && oldClass.indexOf(' ' + withOut + ' ') != -1) {
|
|
return false;
|
|
}
|
|
}
|
|
if (className instanceof Array) {
|
|
for (var i = 0; i < className.length; i++) {
|
|
if (oldClass.indexOf(' ' + className[i] + ' ') != -1) {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
if (className && oldClass.indexOf(' ' + className + ' ') != -1) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
// filter nodes with any class(es)
|
|
// `nodes` Array of DOM nodes
|
|
// @see `hasAnyClass`
|
|
withAnyClass: function(nodes, className, withOut) {
|
|
var filter = [];
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
if (this.hasAnyClass(nodes[i], className, withOut)) {
|
|
filter.push(nodes[i]);
|
|
}
|
|
}
|
|
return filter;
|
|
},
|
|
// add class(es) to node
|
|
// `node` must be valid DOM node
|
|
// `className` String or Array to add
|
|
// return TRUE if className changed
|
|
addClass: function(node, className) {
|
|
var oldClass = ' ' + node.className + ' ', append = '';
|
|
if (className instanceof Array) {
|
|
for (var i = 0; i < className.length; i++) {
|
|
if (oldClass.indexOf(' ' + className[i] + ' ') == -1) {
|
|
append += ' ' + className[i];
|
|
}
|
|
}
|
|
} else {
|
|
if (oldClass.indexOf(' ' + className + ' ') == -1) {
|
|
append += ' ' + className;
|
|
}
|
|
}
|
|
if (append) {
|
|
node.className = node.className + append;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
// add class(es) to nodes
|
|
// `nodes` Array of DOM nodes
|
|
// @see `addClass`
|
|
addListClass: function(nodes, className, callback) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
this.addClass(nodes[i], className);
|
|
if (callback) {
|
|
callback.call(this, nodes[i]);
|
|
}
|
|
}
|
|
},
|
|
// remove class(es) from node
|
|
// `node` must be valid DOM node
|
|
// `className` String or Array to remove
|
|
// return TRUE if className changed
|
|
removeClass: function(node, className) {
|
|
var oldClass = ' ' + node.className + ' ';
|
|
if (className instanceof Array) {
|
|
for (var i = 0; i < className.length; i++) {
|
|
oldClass = oldClass.replace(' ' + className[i] + ' ', ' ');
|
|
}
|
|
} else {
|
|
oldClass = oldClass.replace(' ' + className + ' ', ' ');
|
|
}
|
|
oldClass = oldClass.substr(1, oldClass.length - 2);
|
|
if (node.className != oldClass) {
|
|
node.className = oldClass;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
// remove class(es) from nodes
|
|
// `nodes` Array of DOM nodes
|
|
// @see `removeClass`
|
|
removeListClass: function(nodes, className, callback) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
this.removeClass(nodes[i], className);
|
|
if (callback) {
|
|
callback.call(this, nodes[i]);
|
|
}
|
|
}
|
|
},
|
|
// toggle node class(es)
|
|
// `node` must be valid DOM node
|
|
// `className` String or Array to toggle
|
|
// `add` TRUE to add them
|
|
// return TRUE if className changed
|
|
toggleClass: function(node, className, add) {
|
|
if (add) {
|
|
return this.addClass(node, className);
|
|
} else {
|
|
return this.removeClass(node, className);
|
|
}
|
|
},
|
|
// toggle nodes class(es)
|
|
// `nodes` Array of DOM nodes
|
|
// @see `toggleClass`
|
|
toggleListClass: function(nodes, className, add, callback) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
this.toggleClass(nodes[i], className, add);
|
|
if (callback) {
|
|
callback.call(this, nodes[i]);
|
|
}
|
|
}
|
|
},
|
|
// add/remove and keep old class(es)
|
|
// `node` must be valid DOM node
|
|
// `addClass` String or Array to add
|
|
// `removeClass` String or Array to remove
|
|
// return TRUE if className changed
|
|
addRemoveClass: function(node, addClass, removeClass) {
|
|
var oldClass = ' ' + node.className + ' ';
|
|
if (removeClass) {
|
|
if (removeClass instanceof Array) {
|
|
for (var i = 0; i < removeClass.length; i++) {
|
|
oldClass = oldClass.replace(' ' + removeClass[i] + ' ', ' ');
|
|
}
|
|
} else {
|
|
oldClass = oldClass.replace(' ' + removeClass + ' ', ' ');
|
|
}
|
|
}
|
|
if (addClass) {
|
|
var append = '';
|
|
if (addClass instanceof Array) {
|
|
for (var i = 0; i < addClass.length; i++) {
|
|
if (oldClass.indexOf(' ' + addClass[i] + ' ') == -1) {
|
|
append += addClass[i] + ' ';
|
|
}
|
|
}
|
|
} else {
|
|
if (oldClass.indexOf(' ' + addClass + ' ') == -1) {
|
|
append += addClass + ' ';
|
|
}
|
|
}
|
|
oldClass += append;
|
|
}
|
|
oldClass = oldClass.substr(1, oldClass.length - 2);
|
|
if (node.className != oldClass) {
|
|
node.className = oldClass;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
// add/remove and keep old class(es)
|
|
// `nodes` Array of DOM nodes
|
|
// @see `addRemoveClass`
|
|
addRemoveListClass: function(nodes, addClass, removeClass, callback) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
this.addRemoveClass(nodes[i], addClass, removeClass);
|
|
if (callback) {
|
|
callback.call(this, nodes[i]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* The aciTree core.
|
|
*
|
|
* A few words about how item data looks like:
|
|
*
|
|
* for a leaf node (a node that does not have any children):
|
|
*
|
|
* {
|
|
* id: 'some_file_ID', // should be unique item ID
|
|
* label: 'This is a File Item', // the item label (text value)
|
|
* inode: false, // FALSE means is a leaf node (can be omitted)
|
|
* icon: 'fileIcon', // CSS class name for the icon (if any), can also be an Array ['CSS class name', background-position-x, background-position-y]
|
|
* disabled: false, // TRUE means the item is disabled (can be omitted)
|
|
* random_prop: 'random 1' // sample user defined property (you can have any number defined)
|
|
* }
|
|
*
|
|
* for a inner node (a node that have at least a children under it):
|
|
*
|
|
* {
|
|
* id: 'some_folder_ID', // should be unique item ID
|
|
* label: 'This is a Folder Item', // the item label (text value)
|
|
* inode: true, // can also be NULL to find at runtime if its an inode (on load will be transformed in a leaf node if there aren't any children)
|
|
* open: false, // if TRUE then the node will be opened when the tree is loaded (can be omitted)
|
|
* icon: 'folderIcon', // CSS class name for the icon (if any), can also be an Array ['CSS class name', background-position-x, background-position-y]
|
|
* disabled: false, // TRUE means the item is disabled (can be omitted)
|
|
* source: 'myDataSource', // the data source name (if any) to read the children from, by default `aciTree.options.ajax` is used
|
|
* branch: [ // a list of children
|
|
* { ... item data ... },
|
|
* { ... item data ... },
|
|
* ...
|
|
* ],
|
|
* random_prop: 'random 2' // sample user defined property (you can have any number defined)
|
|
* }
|
|
*
|
|
* The `branch` array can be empty, in this case the children will be loaded when the node will be opened for the first time.
|
|
*
|
|
* Please note that the item data should be valid (in the expected format). No checking is done and errors can appear on invalid data.
|
|
*
|
|
* One note about a item: a item is always the LI element with the class 'aciTreeLi'.
|
|
* The children of a node are all added under a UL element with the class 'aciTreeUl'.
|
|
*
|
|
* Almost all API functions expect only one item. If you need to process more at once then you'll need to loop between all of them yourself.
|
|
*
|
|
* The `options` parameter for all API methods (when there is one) is a object with the properties (not all are required or used):
|
|
*
|
|
* {
|
|
* uid: string -> operation UID (defaults to `ui`)
|
|
* success: function (item, options) -> callback to be called on success (you can access plugin API with `this` keyword inside the callback)
|
|
* fail: function (item, options) -> callback to be called on fail (you can access plugin API with `this` keyword inside the callback)
|
|
* notify: function (item, options) -> notify callback (internal use for when already in the requested state, will call `success` by default)
|
|
* expand: true/false -> propagate on open/toggle
|
|
* collapse: true/false -> propagate on close/toggle
|
|
* unique: true/false -> should other branches be closed (on open/toggle) ?
|
|
* unanimated: true/false -> if it's TRUE then no animations are to be run (used on open/close/toggle)
|
|
* itemData: object[item data]/array[item data] -> used when adding/updating items
|
|
* }
|
|
*
|
|
* Note: when using the API methods that support the `options` parameter, you will need to use the success/fail callbacks if you need to do
|
|
* any processing after the API call. This because there can be async operations that will complete at a later time and the API methods will
|
|
* exit before the job is actually completed. This will happen when items are loaded with AJAX, on animations and other delayed operations (see _queue).
|
|
*
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// default options
|
|
|
|
var options = {
|
|
// the AJAX options (see jQuery.ajax) where the `success` and `error` are overridden by aciTree
|
|
ajax: {
|
|
url: null, // URL from where to take the data, something like `path/script?nodeId=` (the node ID value will be added for each request)
|
|
dataType: 'json'
|
|
},
|
|
dataSource: null, // a list of data sources to be used (each entry in `aciTree.options.ajax` format)
|
|
rootData: null, // initial ROOT data for the Tree (if NULL then one initial AJAX request is made on init)
|
|
queue: {
|
|
async: 4, // the number of simultaneous async (AJAX) tasks
|
|
interval: 50, // interval [ms] after which to insert a `delay`
|
|
delay: 20 // how many [ms] delay between tasks (after `interval` expiration)
|
|
},
|
|
loaderDelay: 500, // how many msec to wait before showing the main loader ? (on lengthy operations)
|
|
expand: false, // if TRUE then all children of a node are expanded when the node is opened
|
|
collapse: false, // if TRUE then all children of a node are collapsed when the node is closed
|
|
unique: false, // if TRUE then a single tree branch will stay open, the oters are closed when a node is opened
|
|
empty: false, // if TRUE then all children of a node are removed when the node is closed
|
|
show: {// show node/ROOT animation (default is slideDown)
|
|
props: {
|
|
'height': 'show'
|
|
},
|
|
duration: 'medium',
|
|
easing: 'linear'
|
|
},
|
|
animateRoot: true, // if the ROOT should be animated on init
|
|
hide: {// hide node animation (default is slideUp)
|
|
props: {
|
|
'height': 'hide'
|
|
},
|
|
duration: 'medium',
|
|
easing: 'linear'
|
|
},
|
|
view: {// scroll item into view animation
|
|
duration: 'medium',
|
|
easing: 'linear'
|
|
},
|
|
// called for each AJAX request when a node needs to be loaded
|
|
// `item` is the item who will be loaded
|
|
// `settings` is the `aciTree.options.ajax` object or an entry from `aciTree.options.dataSource`
|
|
ajaxHook: function(item, settings) {
|
|
// the default implementation changes the URL by adding the item ID at the end
|
|
settings.url += (item ? this.getId(item) : '');
|
|
},
|
|
// called after each item is created but before is inserted into the DOM
|
|
// `parent` is the parent item (can be empty)
|
|
// `item` is the new created item
|
|
// `itemData` is the object used to create the item
|
|
// `level` is the #0 based item level
|
|
itemHook: function(parent, item, itemData, level) {
|
|
// there is no default implementation
|
|
},
|
|
// called for each item to serialize its value
|
|
// `item` is the tree item to be serialized
|
|
// `what` is the option telling what is being serialized
|
|
// `value` is the current serialized value (from the `item`, value type depending of `what`)
|
|
serialize: function(item, what, value) {
|
|
if (typeof what == 'object') {
|
|
return value;
|
|
} else {
|
|
// the default implementation uses a `|` (pipe) character to separate values
|
|
return '|' + value;
|
|
}
|
|
}
|
|
};
|
|
|
|
// aciTree plugin core
|
|
|
|
var aciTree_core = {
|
|
// add extra data
|
|
__extend: function() {
|
|
$.extend(this._instance, {
|
|
queue: new this._queue(this, this._instance.options.queue) // the global tree queue
|
|
});
|
|
$.extend(this._private, {
|
|
locked: false, // to tell the tree state
|
|
itemClone: {// keep a clone of the LI for faster tree item creation
|
|
},
|
|
// timeouts for the loader
|
|
loaderHide: null,
|
|
loaderInterval: null,
|
|
// busy delay counter
|
|
delayBusy: 0
|
|
});
|
|
},
|
|
// init the treeview
|
|
init: function(options) {
|
|
options = this._options(options);
|
|
// check if was init already
|
|
if (this.wasInit()) {
|
|
this._trigger(null, 'wasinit', options);
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
// check if is locked
|
|
if (this.isLocked()) {
|
|
this._trigger(null, 'locked', options);
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
// a way to cancel the operation
|
|
if (!this._trigger(null, 'beforeinit', options)) {
|
|
this._trigger(null, 'initfail', options);
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
this._private.locked = true;
|
|
this._instance.jQuery.addClass('aciTree' + this._instance.index).attr('role', 'tree').on('click' + this._instance.nameSpace, '.aciTreeButton', this.proxy(function(e) {
|
|
// process click on button
|
|
var item = this.itemFrom(e.target);
|
|
// skip when busy
|
|
if (!this.isBusy(item)) {
|
|
// tree button pressed
|
|
this.toggle(item, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
}
|
|
})).on('mouseenter' + this._instance.nameSpace + ' mouseleave' + this._instance.nameSpace, '.aciTreePush', function(e) {
|
|
// handle the aciTreeHover class
|
|
var element = e.target;
|
|
if (!domApi.hasClass(element, 'aciTreePush')) {
|
|
element = domApi.parentByClass(element, 'aciTreePush');
|
|
}
|
|
domApi.toggleClass(element, 'aciTreeHover', e.type == 'mouseenter');
|
|
}).on('mouseenter' + this._instance.nameSpace + ' mouseleave' + this._instance.nameSpace, '.aciTreeLine', function(e) {
|
|
// handle the aciTreeHover class
|
|
var element = e.target;
|
|
if (!domApi.hasClass(element, 'aciTreeLine')) {
|
|
element = domApi.parentByClass(element, 'aciTreeLine');
|
|
}
|
|
domApi.toggleClass(element, 'aciTreeHover', e.type == 'mouseenter');
|
|
});
|
|
this._initHook();
|
|
// call on success
|
|
var success = this.proxy(function() {
|
|
// call the parent
|
|
this._super();
|
|
this._private.locked = false;
|
|
this._trigger(null, 'init', options);
|
|
this._success(null, options);
|
|
});
|
|
// call on fail
|
|
var fail = this.proxy(function() {
|
|
// call the parent
|
|
this._super();
|
|
this._private.locked = false;
|
|
this._trigger(null, 'initfail', options);
|
|
this._fail(null, options);
|
|
});
|
|
if (this._instance.options.rootData) {
|
|
// the rootData was set, use it to init the tree
|
|
this.loadFrom(null, this._inner(options, {
|
|
success: success,
|
|
fail: fail,
|
|
itemData: this._instance.options.rootData
|
|
}));
|
|
} else if (this._instance.options.ajax.url) {
|
|
// the AJAX url was set, init with AJAX
|
|
this.ajaxLoad(null, this._inner(options, {
|
|
success: success,
|
|
fail: fail
|
|
}));
|
|
} else {
|
|
success.apply(this);
|
|
}
|
|
},
|
|
_initHook: function() {
|
|
// override this to do extra init
|
|
},
|
|
// check locked state
|
|
isLocked: function() {
|
|
return this._private.locked;
|
|
},
|
|
// get a formatted message
|
|
// `raw` is the raw message text (can contain %NUMBER sequences, replaced with values from `params`)
|
|
// `params` is a list of values to be replaced into the message (by #0 based index)
|
|
_format: function(raw, params) {
|
|
if (!(params instanceof Array)) {
|
|
return raw;
|
|
}
|
|
var parts = raw.split(/(%[0-9]+)/gm);
|
|
var compile = '', part, index, last = false, len;
|
|
var test = new window.RegExp('^%[0-9]+$');
|
|
for (var i = 0; i < parts.length; i++) {
|
|
part = parts[i];
|
|
len = part.length;
|
|
if (len) {
|
|
if (!last && test.test(part)) {
|
|
index = window.parseInt(part.substr(1)) - 1;
|
|
if ((index >= 0) && (index < params.length)) {
|
|
compile += params[index];
|
|
continue;
|
|
}
|
|
} else {
|
|
last = false;
|
|
if (part.substr(len - 1) == '%') {
|
|
if (part.substr(len - 2) != '%%') {
|
|
last = true;
|
|
}
|
|
part = part.substr(0, len - 1);
|
|
}
|
|
}
|
|
compile += part;
|
|
}
|
|
}
|
|
return compile;
|
|
},
|
|
// low level DOM functions
|
|
_coreDOM: {
|
|
// set as leaf node
|
|
leaf: function(items) {
|
|
domApi.addRemoveListClass(items.toArray(), 'aciTreeLeaf', ['aciTreeInode', 'aciTreeInodeMaybe', 'aciTreeOpen'], function(node) {
|
|
node.firstChild.removeAttribute('aria-expanded');
|
|
});
|
|
},
|
|
// set as inner node
|
|
inode: function(items, branch) {
|
|
domApi.addRemoveListClass(items.toArray(), branch ? 'aciTreeInode' : 'aciTreeInodeMaybe', 'aciTreeLeaf', function(node) {
|
|
node.firstChild.setAttribute('aria-expanded', false);
|
|
});
|
|
},
|
|
// set as open/closed
|
|
toggle: function(items, state) {
|
|
domApi.toggleListClass(items.toArray(), 'aciTreeOpen', state, function(node) {
|
|
node.firstChild.setAttribute('aria-expanded', state);
|
|
});
|
|
},
|
|
// set odd/even classes
|
|
oddEven: function(items, odd) {
|
|
var list = items.toArray();
|
|
for (var i = 0; i < list.length; i++) {
|
|
domApi.addRemoveClass(list[i], odd ? 'aciTreeOdd' : 'aciTreeEven', odd ? 'aciTreeEven' : 'aciTreeOdd');
|
|
odd = !odd;
|
|
}
|
|
}
|
|
},
|
|
// a small queue implementation
|
|
// `context` the context to be used with `callback.call`
|
|
// `options` are the queue options
|
|
_queue: function(context, options) {
|
|
var locked = false;
|
|
var fifo = [], fifoAsync = [];
|
|
var load = 0, loadAsync = 0, schedule = 0, stack = 0;
|
|
// run the queue
|
|
var run = function() {
|
|
if (locked) {
|
|
stack--;
|
|
return;
|
|
}
|
|
var now = new window.Date().getTime();
|
|
if (schedule > now) {
|
|
stack--;
|
|
return;
|
|
}
|
|
var callback, async = false;
|
|
if (load < options.async * 2) {
|
|
// get the next synchronous callback
|
|
callback = fifo.shift();
|
|
}
|
|
if (!callback && (loadAsync < options.async)) {
|
|
// get the next async callback
|
|
callback = fifoAsync.shift();
|
|
async = true;
|
|
}
|
|
if (callback) {
|
|
// run the callback
|
|
if (async) {
|
|
loadAsync++;
|
|
callback.call(context, function() {
|
|
loadAsync--;
|
|
});
|
|
if (stack < 40) {
|
|
stack++;
|
|
run();
|
|
}
|
|
} else {
|
|
load++;
|
|
callback.call(context, function() {
|
|
if (now - schedule > options.interval) {
|
|
schedule = now + options.delay;
|
|
}
|
|
load--;
|
|
if (stack < 40) {
|
|
stack++;
|
|
run();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
stack--;
|
|
};
|
|
var interval = [];
|
|
// start the queue
|
|
var start = function() {
|
|
for (var i = 0; i < 4; i++) {
|
|
interval[i] = window.setInterval(function() {
|
|
if (stack < 20) {
|
|
stack++;
|
|
run();
|
|
}
|
|
}, 10);
|
|
}
|
|
};
|
|
// stop the queue
|
|
var stop = function() {
|
|
for (var i = 0; i < interval.length; i++) {
|
|
window.clearInterval(interval[i]);
|
|
}
|
|
};
|
|
start();
|
|
// init the queue
|
|
this.init = function() {
|
|
this.destroy();
|
|
start();
|
|
return this;
|
|
};
|
|
// push `callback` function (complete) for later call
|
|
// `async` tells if is async callback
|
|
this.push = function(callback, async) {
|
|
if (!locked) {
|
|
if (async) {
|
|
fifoAsync.push(callback);
|
|
} else {
|
|
fifo.push(callback);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
// test if busy
|
|
this.busy = function() {
|
|
return (load != 0) || (loadAsync != 0) || (fifo.length != 0) || (fifoAsync.length != 0);
|
|
};
|
|
// destroy queue
|
|
this.destroy = function() {
|
|
locked = true;
|
|
stop();
|
|
fifo = [];
|
|
fifoAsync = [];
|
|
load = 0;
|
|
loadAsync = 0;
|
|
schedule = 0;
|
|
stack = 0;
|
|
locked = false;
|
|
return this;
|
|
};
|
|
},
|
|
// used with a `queue` to execute something at the end
|
|
// `endCallback` function (complete) is the callback called at the end
|
|
_task: function(queue, endCallback) {
|
|
var counter = 0, finish = false;
|
|
// push a `callback` function (complete) for later call
|
|
this.push = function(callback, async) {
|
|
counter++;
|
|
queue.push(function(complete) {
|
|
var context = this;
|
|
callback.call(this, function() {
|
|
counter--;
|
|
if ((counter < 1) && !finish) {
|
|
finish = true;
|
|
endCallback.call(context, complete);
|
|
} else {
|
|
complete();
|
|
}
|
|
});
|
|
}, async);
|
|
};
|
|
},
|
|
// helper function to extend the `options` object
|
|
// `object` the initial options object
|
|
// _success, _fail, _notify are callbacks or string (the event name to be triggered)
|
|
// `item` is the item to trigger events for
|
|
_options: function(object, _success, _fail, _notify, item) {
|
|
// options object (need to be in this form for all API functions
|
|
// that have the `options` parameter, not all properties are required)
|
|
var options = $.extend({
|
|
uid: 'ui',
|
|
success: null, // success callback
|
|
fail: null, // fail callback
|
|
notify: null, // notify callback (internal use for when already in the requested state)
|
|
expand: this._instance.options.expand, // propagate (on open)
|
|
collapse: this._instance.options.collapse, // propagate (on close)
|
|
unique: this._instance.options.unique, // keep a single branch open (on open)
|
|
unanimated: false, // unanimated (open/close/toggle)
|
|
itemData: {
|
|
} // items data (object) or a list (array) of them (used when creating branches)
|
|
},
|
|
object);
|
|
var success = _success ? ((typeof _success == 'string') ? function() {
|
|
this._trigger(item, _success, options);
|
|
} : _success) : null;
|
|
var fail = _fail ? ((typeof _fail == 'string') ? function() {
|
|
this._trigger(item, _fail, options);
|
|
} : _fail) : null;
|
|
var notify = _notify ? ((typeof _notify == 'string') ? function() {
|
|
this._trigger(item, _notify, options);
|
|
} : _notify) : null;
|
|
if (success) {
|
|
// success callback
|
|
if (object && object.success) {
|
|
options.success = function() {
|
|
success.apply(this, arguments);
|
|
object.success.apply(this, arguments);
|
|
};
|
|
} else {
|
|
options.success = success;
|
|
}
|
|
}
|
|
if (fail) {
|
|
// fail callback
|
|
if (object && object.fail) {
|
|
options.fail = function() {
|
|
fail.apply(this, arguments);
|
|
object.fail.apply(this, arguments);
|
|
};
|
|
} else {
|
|
options.fail = fail;
|
|
}
|
|
}
|
|
if (notify) {
|
|
// notify callback
|
|
if (object && object.notify) {
|
|
options.notify = function() {
|
|
notify.apply(this, arguments);
|
|
object.notify.apply(this, arguments);
|
|
};
|
|
} else if (!options.notify && object && object.success) {
|
|
options.notify = function() {
|
|
notify.apply(this, arguments);
|
|
object.success.apply(this, arguments);
|
|
};
|
|
} else {
|
|
options.notify = notify;
|
|
}
|
|
} else if (!options.notify && object && object.success) {
|
|
// by default, run success callback
|
|
options.notify = object.success;
|
|
}
|
|
return options;
|
|
},
|
|
// helper for passing `options` object to inner methods
|
|
// the callbacks are removed and `override` can be used to update properties
|
|
_inner: function(options, override) {
|
|
// removing success/fail/notify from options
|
|
return $.extend({
|
|
}, options, {
|
|
success: null,
|
|
fail: null,
|
|
notify: null
|
|
},
|
|
override);
|
|
},
|
|
// trigger the aciTree events on the tree container
|
|
_trigger: function(item, eventName, options) {
|
|
var event = $.Event('acitree');
|
|
if (!options) {
|
|
options = this._options();
|
|
}
|
|
this._instance.jQuery.trigger(event, [this, item, eventName, options]);
|
|
return !event.isDefaultPrevented();
|
|
},
|
|
// call on success
|
|
_success: function(item, options) {
|
|
if (options && options.success) {
|
|
options.success.call(this, item, options);
|
|
}
|
|
},
|
|
// call on fail
|
|
_fail: function(item, options) {
|
|
if (options && options.fail) {
|
|
options.fail.call(this, item, options);
|
|
}
|
|
},
|
|
// call on notify (should be same as `success` but called when already in the requested state)
|
|
_notify: function(item, options) {
|
|
if (options && options.notify) {
|
|
options.notify.call(this, item, options);
|
|
}
|
|
},
|
|
// delay callback on busy item
|
|
_delayBusy: function(item, callback) {
|
|
if ((this._private.delayBusy < 10) && this.isBusy(item)) {
|
|
this._private.delayBusy++;
|
|
window.setTimeout(this.proxy(function() {
|
|
this._delayBusy.call(this, item, callback);
|
|
this._private.delayBusy--;
|
|
}), 10);
|
|
return;
|
|
}
|
|
callback.apply(this);
|
|
},
|
|
// return the data source for item
|
|
// defaults to `aciTree.options.ajax` if not set on the item/his parents
|
|
_dataSource: function(item) {
|
|
var dataSource = this._instance.options.dataSource;
|
|
if (dataSource) {
|
|
var data = this.itemData(item);
|
|
if (data && data.source && dataSource[data.source]) {
|
|
return dataSource[data.source];
|
|
}
|
|
var parent;
|
|
do {
|
|
parent = this.parent(item);
|
|
data = this.itemData(parent);
|
|
if (data && data.source && dataSource[data.source]) {
|
|
return dataSource[data.source];
|
|
}
|
|
} while (parent.length);
|
|
}
|
|
return this._instance.options.ajax;
|
|
},
|
|
// process item loading with AJAX
|
|
// `item` can be NULL to load the ROOT
|
|
// loaded data need to be array of item objects
|
|
// each item can have children (defined as `itemData.branch` - array of item data objects)
|
|
ajaxLoad: function(item, options) {
|
|
if (item && this.isBusy(item)) {
|
|
// delay the load if busy
|
|
this._delayBusy(item, function() {
|
|
this.ajaxLoad(item, options);
|
|
});
|
|
return;
|
|
}
|
|
options = this._options(options, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'loaded', options);
|
|
}, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'loadfail', options);
|
|
}, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'wasloaded', options);
|
|
});
|
|
if (!item || this.isInode(item)) {
|
|
// add the task to the queue
|
|
this._instance.queue.push(function(complete) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeload', options)) {
|
|
this._fail(item, options);
|
|
complete();
|
|
return;
|
|
}
|
|
this._loading(item, true);
|
|
if (this.wasLoad(item)) {
|
|
// was load already
|
|
this._notify(item, options);
|
|
complete();
|
|
return;
|
|
}
|
|
// ensure we work on a copy of the dataSource object
|
|
var settings = $.extend({
|
|
}, this._dataSource(item));
|
|
// call the `aciTree.options.ajaxHook`
|
|
this._instance.options.ajaxHook.call(this, item, settings);
|
|
// loaded data need to be array of item objects
|
|
settings.success = this.proxy(function(itemList) {
|
|
if (itemList && (itemList instanceof Array) && itemList.length) {
|
|
// the AJAX returned some items
|
|
var process = function() {
|
|
if (this.wasLoad(item)) {
|
|
this._notify(item, options);
|
|
complete();
|
|
} else {
|
|
// create a branch from `itemList`
|
|
this._createBranch(item, this._inner(options, {
|
|
success: function() {
|
|
this._success(item, options);
|
|
complete();
|
|
},
|
|
fail: function() {
|
|
this._fail(item, options);
|
|
complete();
|
|
},
|
|
itemData: itemList
|
|
}));
|
|
}
|
|
};
|
|
if (!item || this.isInode(item)) {
|
|
process.apply(this);
|
|
} else {
|
|
// change the item to inode, then load
|
|
this.setInode(item, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
}
|
|
} else {
|
|
// the AJAX response was not just right (or not a inode)
|
|
var process = function() {
|
|
this._fail(item, options);
|
|
complete();
|
|
};
|
|
if (!item || this.isLeaf(item)) {
|
|
process.apply(this);
|
|
} else {
|
|
// change the item to leaf
|
|
this.setLeaf(item, this._inner(options, {
|
|
success: process,
|
|
fail: process
|
|
}));
|
|
}
|
|
}
|
|
});
|
|
settings.error = this.proxy(function() {
|
|
// AJAX failed
|
|
this._fail(item, options);
|
|
complete();
|
|
});
|
|
$.ajax(settings);
|
|
}, true);
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// process item loading
|
|
// `item` can be NULL to load the ROOT
|
|
// `options.itemData` need to be array of item objects
|
|
// each item can have children (defined as `itemData.branch` - array of item data objects)
|
|
loadFrom: function(item, options) {
|
|
if (item && this.isBusy(item)) {
|
|
// delay the load if busy
|
|
this._delayBusy(item, function() {
|
|
this.loadFrom(item, options);
|
|
});
|
|
return;
|
|
}
|
|
options = this._options(options, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'loaded', options);
|
|
}, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'loadfail', options);
|
|
}, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'wasloaded', options);
|
|
});
|
|
if (!item || this.isInode(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeload', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
this._loading(item, true);
|
|
if (this.wasLoad(item)) {
|
|
// was load already
|
|
this._notify(item, options);
|
|
return;
|
|
}
|
|
// data need to be array of item objects
|
|
if (options.itemData && (options.itemData instanceof Array) && options.itemData.length) {
|
|
// create the branch from `options.itemData`
|
|
var process = function() {
|
|
if (this.wasLoad(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
this._createBranch(item, options);
|
|
}
|
|
};
|
|
if (!item || this.isInode(item)) {
|
|
process.apply(this);
|
|
} else {
|
|
// change the item to inode, then create children
|
|
this.setInode(item, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
}
|
|
} else {
|
|
// this is not a inode
|
|
if (!item || this.isLeaf(item)) {
|
|
this._fail(item, options);
|
|
} else {
|
|
// change the item to leaf
|
|
this.setLeaf(item, this._inner(options, {
|
|
success: options.fail,
|
|
fail: options.fail
|
|
}));
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// unload item
|
|
// `item` can be NULL to unload the entire tree
|
|
unload: function(item, options) {
|
|
options = this._options(options, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'unloaded', options);
|
|
}, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'unloadfail', options);
|
|
}, function() {
|
|
this._loading(item);
|
|
this._trigger(item, 'notloaded', options);
|
|
});
|
|
if (!item || this.isInode(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeunload', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
this._loading(item, true);
|
|
if (!this.wasLoad(item)) {
|
|
// if was not loaded
|
|
this._notify(item, options);
|
|
return;
|
|
}
|
|
// first check each children
|
|
var cancel = false;
|
|
var children = this.children(item, true, true);
|
|
children.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (this.isInode(item)) {
|
|
if (this.isOpen(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeclose', options)) {
|
|
cancel = true;
|
|
return false;
|
|
}
|
|
}
|
|
if (this.wasLoad(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeunload', options)) {
|
|
cancel = true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeremove', options)) {
|
|
cancel = true;
|
|
return false;
|
|
}
|
|
}, true));
|
|
if (cancel) {
|
|
// it was canceled
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var process = function() {
|
|
children.each(this.proxy(function(element) {
|
|
// trigger the events before DOM changes
|
|
var item = $(element);
|
|
if (this.isInode(item)) {
|
|
if (this.isOpen(item)) {
|
|
this._trigger(item, 'closed', options);
|
|
}
|
|
if (this.wasLoad(item)) {
|
|
this._trigger(item, 'unloaded', options);
|
|
}
|
|
}
|
|
this._trigger(item, 'removed', options);
|
|
}, true));
|
|
};
|
|
// process the child remove
|
|
if (item) {
|
|
if (this.isOpen(item)) {
|
|
// first close the item, then remove children
|
|
this.close(item, this._inner(options, {
|
|
success: function() {
|
|
process.call(this);
|
|
this._removeContainer(item);
|
|
this._success(item, options);
|
|
},
|
|
fail: options.fail
|
|
}));
|
|
} else {
|
|
process.call(this);
|
|
this._removeContainer(item);
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
// unload the ROOT
|
|
this._animate(item, false, !this._instance.options.animateRoot || options.unanimated, function() {
|
|
process.call(this);
|
|
this._removeContainer();
|
|
this._success(item, options);
|
|
});
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// remove item
|
|
remove: function(item, options) {
|
|
if (this.isItem(item)) {
|
|
if (this.hasSiblings(item, true)) {
|
|
options = this._options(options, function() {
|
|
if (this.isOpenPath(item)) {
|
|
// if the parents are opened (visible) update the item states
|
|
domApi.removeClass(item[0], 'aciTreeVisible');
|
|
this._setOddEven(item);
|
|
}
|
|
this._trigger(item, 'removed', options);
|
|
}, 'removefail', null, item);
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeremove', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.wasLoad(item)) {
|
|
// unload the inode then remove
|
|
this.unload(item, this._inner(options, {
|
|
success: function() {
|
|
this._success(item, options);
|
|
this._removeItem(item);
|
|
},
|
|
fail: options.fail
|
|
}));
|
|
} else {
|
|
// just remove the item
|
|
this._success(item, options);
|
|
this._removeItem(item);
|
|
}
|
|
} else {
|
|
var parent = this.parent(item);
|
|
if (parent.length) {
|
|
this.setLeaf(parent, options);
|
|
} else {
|
|
this.unload(null, options);
|
|
}
|
|
}
|
|
} else {
|
|
this._trigger(item, 'removefail', options)
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// open item children
|
|
_openChildren: function(item, options) {
|
|
if (options.expand) {
|
|
var queue = this._instance.queue;
|
|
// process the children inodes
|
|
this.inodes(this.children(item)).each(function() {
|
|
var item = $(this);
|
|
// queue node opening
|
|
queue.push(function(complete) {
|
|
this.open(item, this._inner(options));
|
|
complete();
|
|
});
|
|
});
|
|
queue.push(function(complete) {
|
|
this._success(item, options);
|
|
complete();
|
|
});
|
|
} else {
|
|
this._success(item, options);
|
|
}
|
|
},
|
|
// process item open
|
|
_openItem: function(item, options) {
|
|
if (!options.unanimated && !this.isVisible(item)) {
|
|
options.unanimated = true;
|
|
}
|
|
if (options.unique) {
|
|
// close other opened nodes
|
|
this.closeOthers(item);
|
|
options.unique = false;
|
|
}
|
|
// open the node
|
|
this._coreDOM.toggle(item, true);
|
|
// (temporarily) update children states
|
|
this._setOddEvenChildren(item);
|
|
this._animate(item, true, options.unanimated, function() {
|
|
this._openChildren(item, options);
|
|
});
|
|
},
|
|
// open item and his children if requested
|
|
open: function(item, options) {
|
|
options = this._options(options, function() {
|
|
if (this.isOpenPath(item)) {
|
|
// if all parents are open, update the items after
|
|
this._updateVisible(item);
|
|
this._setOddEven(item);
|
|
}
|
|
this._trigger(item, 'opened', options);
|
|
}, 'openfail', 'wasopened', item);
|
|
if (this.isInode(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeopen', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isOpen(item)) {
|
|
options.success = options.notify;
|
|
// propagate/open children (if required)
|
|
this._openChildren(item, options);
|
|
} else {
|
|
if (this.wasLoad(item)) {
|
|
this._openItem(item, options);
|
|
} else {
|
|
// try to load the node, then open
|
|
this.ajaxLoad(item, this._inner(options, {
|
|
success: function() {
|
|
this._openItem(item, options);
|
|
},
|
|
fail: options.fail
|
|
}));
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// close item children
|
|
_closeChildren: function(item, options) {
|
|
if (this._instance.options.empty) {
|
|
// unload on close
|
|
options.unanimated = true;
|
|
this.unload(item, options);
|
|
} else if (options.collapse) {
|
|
var queue = this._instance.queue;
|
|
// process the children inodes
|
|
this.inodes(this.children(item)).each(function() {
|
|
var item = $(this);
|
|
// queue node close
|
|
queue.push(function(complete) {
|
|
this.close(item, this._inner(options, {
|
|
unanimated: true
|
|
}));
|
|
complete();
|
|
});
|
|
});
|
|
queue.push(function(complete) {
|
|
this._success(item, options);
|
|
complete();
|
|
});
|
|
} else {
|
|
this._success(item, options);
|
|
}
|
|
},
|
|
// process item close
|
|
_closeItem: function(item, options) {
|
|
if (!options.unanimated && !this.isVisible(item)) {
|
|
options.unanimated = true;
|
|
}
|
|
// close the item
|
|
this._coreDOM.toggle(item, false);
|
|
this._animate(item, false, options.unanimated, function() {
|
|
this._closeChildren(item, options);
|
|
});
|
|
},
|
|
// close item and his children if requested
|
|
close: function(item, options) {
|
|
options = this._options(options, function() {
|
|
if (this.isOpenPath(item)) {
|
|
// if all parents are open, update the items after
|
|
this._updateVisible(item);
|
|
this._setOddEven(item);
|
|
}
|
|
this._trigger(item, 'closed', options);
|
|
}, 'closefail', 'wasclosed', item);
|
|
if (this.isInode(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeclose', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isOpen(item)) {
|
|
this._closeItem(item, options);
|
|
} else if (this.wasLoad(item)) {
|
|
options.success = options.notify;
|
|
// propagate/close/empty children (if required)
|
|
this._closeChildren(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// update visible state
|
|
_updateVisible: function(item) {
|
|
if (this.isOpenPath(item)) {
|
|
if (!this.isHidden(item)) {
|
|
// if open parents and not hidden
|
|
domApi.addClass(item[0], 'aciTreeVisible');
|
|
if (this.isOpen(item)) {
|
|
// process children
|
|
domApi.children(item[0], false, this.proxy(function(node) {
|
|
if (!domApi.hasClass(node, 'aciTreeVisible')) {
|
|
this._updateVisible($(node));
|
|
}
|
|
}));
|
|
} else {
|
|
// children are not visible
|
|
domApi.children(item[0], true, function(node) {
|
|
return domApi.removeClass(node, 'aciTreeVisible') ? true : null;
|
|
});
|
|
}
|
|
}
|
|
} else if (domApi.removeClass(item[0], 'aciTreeVisible')) {
|
|
domApi.children(item[0], true, function(node) {
|
|
return domApi.removeClass(node, 'aciTreeVisible') ? true : null;
|
|
});
|
|
}
|
|
},
|
|
// keep just one branch open
|
|
closeOthers: function(item, options) {
|
|
options = this._options(options);
|
|
if (this.isItem(item)) {
|
|
var queue = this._instance.queue;
|
|
// exclude the item and his parents
|
|
var exclude = item.add(this.path(item)).add(this.children(item, true));
|
|
// close all other open nodes
|
|
this.inodes(this.children(null, true, true), true).not(exclude).each(function() {
|
|
var item = $(this);
|
|
// add node to close queue
|
|
queue.push(function(complete) {
|
|
this.close(item, this._inner(options));
|
|
complete();
|
|
});
|
|
});
|
|
queue.push(function(complete) {
|
|
this._success(item, options);
|
|
complete();
|
|
});
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// toggle item
|
|
toggle: function(item, options) {
|
|
options = this._options(options, 'toggled', 'togglefail', null, item);
|
|
if (this.isInode(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforetoggle', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isOpen(item)) {
|
|
this.close(item, options);
|
|
} else {
|
|
this.open(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// get item path starting from the top parent (ROOT)
|
|
// when `reverse` is TRUE returns the path in reverse order
|
|
path: function(item, reverse) {
|
|
if (item) {
|
|
var parent = item[0], list = [];
|
|
while (parent = domApi.parent(parent)) {
|
|
list.push(parent);
|
|
}
|
|
return reverse ? $(list) : $(list.reverse());
|
|
}
|
|
return $([]);
|
|
},
|
|
// test if item is in view
|
|
// when `center` is TRUE will test if is centered in view
|
|
isVisible: function(item, center) {
|
|
if (item && domApi.hasClass(item[0], 'aciTreeVisible')) {
|
|
// the item path need to be open
|
|
var rect = this._instance.jQuery[0].getBoundingClientRect();
|
|
var size = item[0].firstChild;
|
|
var test = size.getBoundingClientRect();
|
|
var height = size.offsetHeight;
|
|
var offset = center ? (rect.bottom - rect.top) / 2 : 0;
|
|
if ((test.bottom - height < rect.top + offset) || (test.top + height > rect.bottom - offset)) {
|
|
// is out of view
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
// open path to item
|
|
openPath: function(item, options) {
|
|
options = this._options(options);
|
|
if (this.isItem(item)) {
|
|
var queue = this._instance.queue;
|
|
// process closed inodes
|
|
this.inodes(this.path(item), false).each(function() {
|
|
var item = $(this);
|
|
// add node to open queue
|
|
queue.push(function(complete) {
|
|
this.open(item, this._inner(options));
|
|
complete();
|
|
});
|
|
});
|
|
queue.push(function(complete) {
|
|
this._success(item, options);
|
|
complete();
|
|
});
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if path to item is open
|
|
isOpenPath: function(item) {
|
|
var parent = this.parent(item);
|
|
return parent[0] ? this.isOpen(parent) && domApi.hasClass(parent[0], 'aciTreeVisible') : true;
|
|
},
|
|
// get animation speed vs. offset size
|
|
// `speed` is the raw speed
|
|
// `totalSize` is the available size
|
|
// `required` is the offset used for calculations
|
|
_speedFraction: function(speed, totalSize, required) {
|
|
if ((required < totalSize) && totalSize) {
|
|
var numeric = parseInt(speed);
|
|
if (isNaN(numeric)) {
|
|
// predefined string values
|
|
switch (speed) {
|
|
case 'slow':
|
|
numeric = 600;
|
|
break;
|
|
case 'medium':
|
|
numeric = 400;
|
|
break;
|
|
case 'fast':
|
|
numeric = 200;
|
|
break;
|
|
default:
|
|
return speed;
|
|
}
|
|
}
|
|
return numeric * required / totalSize;
|
|
}
|
|
return speed;
|
|
},
|
|
// bring item in view
|
|
// `options.center` says if should be centered in view
|
|
setVisible: function(item, options) {
|
|
options = this._options(options, 'visible', 'visiblefail', 'wasvisible', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforevisible', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isVisible(item)) {
|
|
// is visible already
|
|
this._notify(item, options);
|
|
return;
|
|
}
|
|
var process = function() {
|
|
// compute position with getBoundingClientRect
|
|
var rect = this._instance.jQuery[0].getBoundingClientRect();
|
|
var size = item[0].firstChild;
|
|
var test = size.getBoundingClientRect();
|
|
var height = size.offsetHeight;
|
|
var offset = options.center ? (rect.bottom - rect.top) / 2 : 0;
|
|
if (test.bottom - height < rect.top + offset) {
|
|
// item somewhere before the first visible
|
|
var diff = rect.top + offset - test.bottom + height;
|
|
if (!options.unanimated && this._instance.options.view) {
|
|
this._instance.jQuery.stop(true).animate({
|
|
scrollTop: this._instance.jQuery.scrollTop() - diff
|
|
},
|
|
{
|
|
duration: this._speedFraction(this._instance.options.view.duration, rect.bottom - rect.top, diff),
|
|
easing: this._instance.options.view.easing,
|
|
complete: this.proxy(function() {
|
|
this._success(item, options);
|
|
})
|
|
});
|
|
} else {
|
|
this._instance.jQuery.stop(true)[0].scrollTop = this._instance.jQuery.scrollTop() - diff;
|
|
this._success(item, options);
|
|
}
|
|
} else if (test.top + height > rect.bottom - offset) {
|
|
// item somewhere after the last visible
|
|
var diff = test.top - rect.bottom + offset + height;
|
|
if (!options.unanimated && this._instance.options.view) {
|
|
this._instance.jQuery.stop(true).animate({
|
|
scrollTop: this._instance.jQuery.scrollTop() + diff
|
|
},
|
|
{
|
|
duration: this._speedFraction(this._instance.options.view.duration, rect.bottom - rect.top, diff),
|
|
easing: this._instance.options.view.easing,
|
|
complete: this.proxy(function() {
|
|
this._success(item, options);
|
|
})
|
|
});
|
|
} else {
|
|
this._instance.jQuery.stop(true)[0].scrollTop = this._instance.jQuery.scrollTop() + diff;
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._success(item, options);
|
|
}
|
|
};
|
|
if (this.hasParent(item)) {
|
|
// first we need to open the path to item
|
|
this.openPath(item, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
} else {
|
|
process.apply(this);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if item has parent
|
|
hasParent: function(item) {
|
|
return this.parent(item).length > 0;
|
|
},
|
|
// get item parent
|
|
parent: function(item) {
|
|
return item ? $(domApi.parent(item[0])) : $([]);
|
|
},
|
|
// get item top (ROOT) parent
|
|
topParent: function(item) {
|
|
return this.path(item).eq(0);
|
|
},
|
|
// create tree branch
|
|
// `options.itemData` need to be in the same format as for .append
|
|
_createBranch: function(item, options) {
|
|
var total = 0;
|
|
var count = function(itemList) {
|
|
var itemData;
|
|
for (var i = 0; i < itemList.length; i++) {
|
|
itemData = itemList[i];
|
|
if (itemData.branch && (itemData.branch instanceof Array) && itemData.branch.length) {
|
|
count(itemData.branch);
|
|
}
|
|
}
|
|
total++;
|
|
};
|
|
count(options.itemData);
|
|
var index = 0;
|
|
var complete = this.proxy(function() {
|
|
index++;
|
|
if (index >= total) {
|
|
this._success(item, options);
|
|
}
|
|
});
|
|
var process = this.proxy(function(node, itemList) {
|
|
if (node) {
|
|
// set it as a inode
|
|
domApi.addRemoveClass(node[0], 'aciTreeInode', 'aciTreeInodeMaybe');
|
|
}
|
|
// use .append to add new items
|
|
this.append(node, this._inner(options, {
|
|
success: function(item, options) {
|
|
var itemData;
|
|
for (var i = 0; i < options.itemData.length; i++) {
|
|
itemData = options.itemData[i];
|
|
// children need to be array of item objects
|
|
if (itemData.branch && (itemData.branch instanceof Array) && itemData.branch.length) {
|
|
process(options.items.eq(i), itemData.branch);
|
|
}
|
|
if (itemData.open) {
|
|
// open the item is requuested
|
|
this.open(options.items.eq(i), this._inner(options, {
|
|
itemData: null,
|
|
items: null
|
|
}));
|
|
}
|
|
}
|
|
complete();
|
|
},
|
|
fail: options.fail,
|
|
itemData: itemList
|
|
}));
|
|
});
|
|
process(item, options.itemData);
|
|
},
|
|
// get first/last items
|
|
_getFirstLast: function(parent) {
|
|
if (!parent) {
|
|
parent = this._instance.jQuery;
|
|
}
|
|
return $(domApi.withAnyClass(domApi.children(parent[0]), ['aciTreeFirst', 'aciTreeLast']));
|
|
},
|
|
// update first/last items
|
|
_setFirstLast: function(parent, clear) {
|
|
if (clear) {
|
|
domApi.removeListClass(clear.toArray(), ['aciTreeFirst', 'aciTreeLast']);
|
|
}
|
|
var first = this.first(parent);
|
|
if (first[0]) {
|
|
domApi.addClass(first[0], 'aciTreeFirst');
|
|
domApi.addClass(this.last(parent)[0], 'aciTreeLast');
|
|
}
|
|
},
|
|
// update odd/even state
|
|
_setOddEven: function(items) {
|
|
// consider only visible items
|
|
var visible;
|
|
if (this._instance.jQuery[0].getElementsByClassName) {
|
|
visible = this._instance.jQuery[0].getElementsByClassName('aciTreeVisible');
|
|
visible = visible ? window.Array.prototype.slice.call(visible) : [];
|
|
} else {
|
|
visible = $(domApi.children(this._instance.jQuery[0], true, function(node) {
|
|
return this.hasClass(node, 'aciTreeVisible') ? true : null;
|
|
}));
|
|
}
|
|
var odd = true;
|
|
if (visible.length) {
|
|
var index = 0;
|
|
if (items) {
|
|
// search the item to start with (by index)
|
|
items.each(function() {
|
|
if (visible.indexOf) {
|
|
var found = visible.indexOf(this);
|
|
if (found != -1) {
|
|
index = window.Math.min(found, index);
|
|
}
|
|
} else {
|
|
for (var i = 0; i < visible.length; i++) {
|
|
if (visible[i] === this) {
|
|
index = window.Math.min(i, index);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
index = window.Math.max(index - 1, 0);
|
|
}
|
|
if (index > 0) {
|
|
// determine with what to start with (odd/even)
|
|
var first = visible[index];
|
|
if (domApi.hasClass(first, 'aciTreOdd')) {
|
|
odd = false;
|
|
}
|
|
// process only after index
|
|
visible = visible.slice(index + 1);
|
|
}
|
|
}
|
|
this._coreDOM.oddEven($(visible), odd);
|
|
},
|
|
// update odd/even state for direct children
|
|
_setOddEvenChildren: function(item) {
|
|
var odd = domApi.hasClass(item[0], 'aciTreeOdd');
|
|
var children = this.children(item);
|
|
this._coreDOM.oddEven(children, !odd);
|
|
},
|
|
// process item before inserting into the DOM
|
|
_itemHook: function(parent, item, itemData, level) {
|
|
if (this._instance.options.itemHook) {
|
|
this._instance.options.itemHook.apply(this, arguments);
|
|
}
|
|
},
|
|
// create item by `itemData`
|
|
// `level` is the #0 based item level
|
|
_createItem: function(itemData, level) {
|
|
if (this._private.itemClone[level]) {
|
|
var li = this._private.itemClone[level].cloneNode(true);
|
|
var line = li.firstChild;
|
|
var icon = line;
|
|
for (var i = 0; i < level; i++) {
|
|
icon = icon.firstChild;
|
|
}
|
|
icon = icon.firstChild.lastChild.firstChild;
|
|
var text = icon.nextSibling;
|
|
} else {
|
|
var li = window.document.createElement('LI');
|
|
li.setAttribute('role', 'presentation');
|
|
var line = window.document.createElement('DIV');
|
|
li.appendChild(line);
|
|
line.setAttribute('tabindex', -1);
|
|
line.setAttribute('role', 'treeitem');
|
|
line.setAttribute('aria-selected', false);
|
|
line.className = 'aciTreeLine';
|
|
var last = line, branch;
|
|
for (var i = 0; i < level; i++) {
|
|
branch = window.document.createElement('DIV');
|
|
last.appendChild(branch);
|
|
branch.className = 'aciTreeBranch aciTreeLevel' + i;
|
|
last = branch;
|
|
}
|
|
var entry = window.document.createElement('DIV');
|
|
last.appendChild(entry);
|
|
entry.className = 'aciTreeEntry';
|
|
var button = window.document.createElement('SPAN');
|
|
entry.appendChild(button);
|
|
button.className = 'aciTreeButton';
|
|
var push = window.document.createElement('SPAN');
|
|
button.appendChild(push);
|
|
push.className = 'aciTreePush';
|
|
push.appendChild(window.document.createElement('SPAN'));
|
|
var item = window.document.createElement('SPAN');
|
|
entry.appendChild(item);
|
|
item.className = 'aciTreeItem';
|
|
var icon = window.document.createElement('SPAN');
|
|
item.appendChild(icon);
|
|
var text = window.document.createElement('SPAN');
|
|
item.appendChild(text);
|
|
text.className = 'aciTreeText';
|
|
this._private.itemClone[level] = li.cloneNode(true);
|
|
}
|
|
li.className = 'aciTreeLi' + (itemData.inode || (itemData.inode === null) ? (itemData.inode || (itemData.branch && itemData.branch.length) ? ' aciTreeInode' : ' aciTreeInodeMaybe') : ' aciTreeLeaf') + ' aciTreeLevel' + level + (itemData.disabled ? ' aciTreeDisabled' : '');
|
|
line.setAttribute('aria-level', level + 1);
|
|
if (itemData.inode || (itemData.inode === null)) {
|
|
line.setAttribute('aria-expanded', false);
|
|
}
|
|
if (itemData.icon) {
|
|
if (itemData.icon instanceof Array) {
|
|
icon.className = 'aciTreeIcon ' + itemData.icon[0];
|
|
icon.style.backgroundPosition = itemData.icon[1] + 'px ' + itemData.icon[2] + 'px';
|
|
} else {
|
|
icon.className = 'aciTreeIcon ' + itemData.icon;
|
|
}
|
|
} else {
|
|
icon.parentNode.removeChild(icon);
|
|
}
|
|
text.innerHTML = itemData.label;
|
|
var $li = $(li);
|
|
$li.data('itemData' + this._instance.nameSpace, $.extend({
|
|
}, itemData, {
|
|
branch: itemData.branch && itemData.branch.length
|
|
}));
|
|
return $li;
|
|
},
|
|
// remove item
|
|
_removeItem: function(item) {
|
|
var parent = this.parent(item);
|
|
item.remove();
|
|
// update sibling state
|
|
this._setFirstLast(parent.length ? parent : null);
|
|
},
|
|
// create & add one or more items
|
|
// `ul`, `before` and `after` are set depending on the caller
|
|
// `itemData` need to be array of objects or just an object (one item)
|
|
// `level` is the #0 based level
|
|
// `callback` function (items) is called at the end of the operation
|
|
_createItems: function(ul, before, after, itemData, level, callback) {
|
|
var items = [], fragment = window.document.createDocumentFragment();
|
|
var task = new this._task(this._instance.queue, function(complete) {
|
|
items = $(items);
|
|
if (items.length) {
|
|
// add the new items
|
|
if (ul) {
|
|
ul[0].appendChild(fragment);
|
|
} else if (before) {
|
|
before[0].parentNode.insertBefore(fragment, before[0]);
|
|
} else if (after) {
|
|
after[0].parentNode.insertBefore(fragment, after[0].nextSibling);
|
|
}
|
|
}
|
|
callback.call(this, items);
|
|
complete();
|
|
});
|
|
if (itemData) {
|
|
this._loader(true);
|
|
var parent;
|
|
if (ul) {
|
|
parent = this.itemFrom(ul);
|
|
} else if (before) {
|
|
parent = this.parent(before);
|
|
} else if (after) {
|
|
parent = this.parent(after);
|
|
}
|
|
if (itemData instanceof Array) {
|
|
// this is a list of items
|
|
for (var i = 0; i < itemData.length; i++) {
|
|
(function(itemData) {
|
|
task.push(function(complete) {
|
|
var item = this._createItem(itemData, level);
|
|
this._itemHook(parent, item, itemData, level);
|
|
fragment.appendChild(item[0]);
|
|
items.push(item[0]);
|
|
complete();
|
|
});
|
|
})(itemData[i]);
|
|
}
|
|
} else {
|
|
task.push(function(complete) {
|
|
// only one item
|
|
var item = this._createItem(itemData, level);
|
|
this._itemHook(parent, item, itemData, level);
|
|
fragment.appendChild(item[0]);
|
|
items.push(item[0]);
|
|
complete();
|
|
});
|
|
}
|
|
}
|
|
// run at least once
|
|
task.push(function(complete) {
|
|
complete();
|
|
});
|
|
},
|
|
// create children container
|
|
_createContainer: function(item) {
|
|
if (!item) {
|
|
item = this._instance.jQuery;
|
|
}
|
|
// ensure we have a UL in place
|
|
var ul = domApi.container(item[0]);
|
|
if (!ul) {
|
|
var ul = window.document.createElement('UL');
|
|
ul.setAttribute('role', 'group');
|
|
ul.className = 'aciTreeUl';
|
|
ul.style.display = 'none';
|
|
item[0].appendChild(ul);
|
|
}
|
|
return $(ul);
|
|
},
|
|
// remove children container
|
|
_removeContainer: function(item) {
|
|
if (!item) {
|
|
item = this._instance.jQuery;
|
|
}
|
|
var ul = domApi.container(item[0]);
|
|
ul.parentNode.removeChild(ul);
|
|
},
|
|
// append one or more items to item
|
|
// `options.itemData` can be a item object or array of item objects
|
|
// `options.items` will keep a list of added items
|
|
append: function(item, options) {
|
|
options = this._options(options, 'appended', 'appendfail', null, item);
|
|
if (item) {
|
|
if (this.isInode(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeappend', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var container = this._createContainer(item);
|
|
var last = this.last(item);
|
|
this._createItems(container, null, null, options.itemData, this.level(item) + 1, function(list) {
|
|
if (list.length) {
|
|
// some items created, update states
|
|
domApi.addRemoveClass(item[0], 'aciTreeInode', 'aciTreeInodeMaybe');
|
|
this._setFirstLast(item, last);
|
|
if (this.isHidden(item)) {
|
|
domApi.addListClass(list.toArray(), 'aciTreeHidden');
|
|
} else if (this.isOpenPath(item) && this.isOpen(item)) {
|
|
domApi.addListClass(list.toArray(), 'aciTreeVisible');
|
|
this._setOddEven(list.first());
|
|
}
|
|
// trigger `added` for each item
|
|
list.each(this.proxy(function(element) {
|
|
this._trigger($(element), 'added', options);
|
|
}, true));
|
|
} else if (!this.hasChildren(item, true)) {
|
|
container.remove();
|
|
}
|
|
options.items = list;
|
|
this._success(item, options);
|
|
});
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
} else {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeappend', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var container = this._createContainer();
|
|
var last = this.last();
|
|
this._createItems(container, null, null, options.itemData, 0, function(list) {
|
|
if (list.length) {
|
|
// some items created, update states
|
|
this._setFirstLast(null, last);
|
|
domApi.addListClass(list.toArray(), 'aciTreeVisible');
|
|
this._setOddEven();
|
|
// trigger `added` for each item
|
|
list.each(this.proxy(function(element) {
|
|
this._trigger($(element), 'added', options);
|
|
}, true));
|
|
this._animate(null, true, !this._instance.options.animateRoot || options.unanimated);
|
|
} else if (!this.hasChildren(null, true)) {
|
|
// remove the children container
|
|
container.remove();
|
|
}
|
|
options.items = list;
|
|
this._success(item, options);
|
|
});
|
|
}
|
|
},
|
|
// insert one or more items before item
|
|
// `options.itemData` can be a item object or array of item objects
|
|
// `options.items` will keep a list of added items
|
|
before: function(item, options) {
|
|
options = this._options(options, 'before', 'beforefail', null, item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforebefore', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var prev = this.prev(item);
|
|
this._createItems(null, item, null, options.itemData, this.level(item), function(list) {
|
|
if (list.length) {
|
|
// some items created, update states
|
|
if (!prev.length) {
|
|
domApi.removeClass(item[0], 'aciTreeFirst');
|
|
domApi.addClass(list.first()[0], 'aciTreeFirst');
|
|
}
|
|
var parent = this.parent(item);
|
|
if (parent.length && this.isHidden(parent)) {
|
|
domApi.addListClass(list.toArray(), 'aciTreeHidden');
|
|
} else if (this.isOpenPath(item)) {
|
|
domApi.addListClass(list.toArray(), 'aciTreeVisible');
|
|
this._setOddEven(list.first());
|
|
}
|
|
// trigger `added` for each item
|
|
list.each(this.proxy(function(element) {
|
|
this._trigger($(element), 'added', options);
|
|
}, true));
|
|
}
|
|
options.items = list;
|
|
this._success(item, options);
|
|
});
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// insert one or more items after item
|
|
// `options.itemData` can be a item object or array of item objects
|
|
// `options.items` will keep a list of added items
|
|
after: function(item, options) {
|
|
options = this._options(options, 'after', 'afterfail', null, item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeafter', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var next = this.next(item);
|
|
this._createItems(null, null, item, options.itemData, this.level(item), function(list) {
|
|
if (list.length) {
|
|
// some items created, update states
|
|
if (!next.length) {
|
|
domApi.removeClass(item[0], 'aciTreeLast');
|
|
domApi.addClass(list.last()[0], 'aciTreeLast');
|
|
}
|
|
var parent = this.parent(item);
|
|
if (parent.length && this.isHidden(parent)) {
|
|
domApi.addListClass(list.toArray(), 'aciTreeHidden');
|
|
} else if (this.isOpenPath(item)) {
|
|
domApi.addListClass(list.toArray(), 'aciTreeVisible');
|
|
this._setOddEven(list.first());
|
|
}
|
|
// trigger `added` for each item
|
|
list.each(this.proxy(function(element) {
|
|
this._trigger($(element), 'added', options);
|
|
}, true));
|
|
}
|
|
options.items = list;
|
|
this._success(item, options);
|
|
});
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// get item having the element
|
|
itemFrom: function(element) {
|
|
if (element) {
|
|
var item = $(element);
|
|
if (item[0] === this._instance.jQuery[0]) {
|
|
return $([]);
|
|
} else {
|
|
return $(domApi.parentFrom(item[0]));
|
|
}
|
|
}
|
|
return $([]);
|
|
},
|
|
// get item children
|
|
// if `branch` is TRUE then all children are returned
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
children: function(item, branch, hidden) {
|
|
return $(domApi.children(item && item[0] ? item[0] : this._instance.jQuery[0], branch, hidden ? null : function(node) {
|
|
return this.hasClass(node, 'aciTreeHidden') ? null : true;
|
|
}));
|
|
},
|
|
// filter only the visible items (items with all parents opened)
|
|
// if `view` is TRUE then only the items in view are returned
|
|
visible: function(items, view) {
|
|
var list = domApi.withClass(items.toArray(), 'aciTreeVisible');
|
|
if (view) {
|
|
var filter = [];
|
|
for (var i = 0; i < list.length; i++) {
|
|
if (this.isVisible($(list[i]))) {
|
|
filter.push(list[i]);
|
|
}
|
|
}
|
|
return $(filter);
|
|
}
|
|
return $(list);
|
|
},
|
|
// filter only inner nodes from items
|
|
// if `state` is set then filter only open/closed ones
|
|
inodes: function(items, state) {
|
|
if (state !== undefined) {
|
|
if (state) {
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeOpen'));
|
|
} else {
|
|
return $(domApi.withAnyClass(items.toArray(), ['aciTreeInode', 'aciTreeInodeMaybe'], 'aciTreeOpen'));
|
|
}
|
|
}
|
|
return $(domApi.withAnyClass(items.toArray(), ['aciTreeInode', 'aciTreeInodeMaybe']));
|
|
},
|
|
// filter only leaf nodes from items
|
|
leaves: function(items) {
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeLeaf'));
|
|
},
|
|
// test if is a inner node
|
|
isInode: function(item) {
|
|
return item && domApi.hasAnyClass(item[0], ['aciTreeInode', 'aciTreeInodeMaybe']);
|
|
},
|
|
// test if is a leaf node
|
|
isLeaf: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeLeaf');
|
|
},
|
|
// test if item was loaded
|
|
wasLoad: function(item) {
|
|
if (item) {
|
|
return domApi.container(item[0]) !== null;
|
|
}
|
|
return domApi.container(this._instance.jQuery[0]) !== null;
|
|
},
|
|
// set item as inner node
|
|
setInode: function(item, options) {
|
|
options = this._options(options, 'inodeset', 'inodefail', 'wasinode', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeinode', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isLeaf(item)) {
|
|
this._coreDOM.inode(item, true);
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// set item as leaf node
|
|
setLeaf: function(item, options) {
|
|
options = this._options(options, 'leafset', 'leaffail', 'wasleaf', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeleaf', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isInode(item)) {
|
|
var process = function() {
|
|
this._coreDOM.leaf(item);
|
|
this._success(item, options);
|
|
};
|
|
if (this.wasLoad(item)) {
|
|
// first unload the node
|
|
this.unload(item, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
} else {
|
|
process.apply(this);
|
|
}
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// add/update item icon
|
|
// `options.icon` can be the CSS class name or array['CSS class name', background-position-x, background-position-y]
|
|
// `options.oldIcon` will keep the old icon
|
|
addIcon: function(item, options) {
|
|
options = this._options(options, 'iconadded', 'addiconfail', 'wasicon', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeaddicon', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var data = this.itemData(item);
|
|
// keep the old one
|
|
options.oldIcon = data.icon;
|
|
var parent = domApi.childrenByClass(item[0].firstChild, 'aciTreeItem');
|
|
var found = domApi.childrenByClass(parent, 'aciTreeIcon');
|
|
if (found && data.icon && (options.icon.toString() == data.icon.toString())) {
|
|
this._notify(item, options);
|
|
} else {
|
|
if (!found) {
|
|
found = window.document.createElement('DIV');
|
|
parent.insertBefore(found, parent.firstChild);
|
|
}
|
|
if (options.icon instanceof Array) {
|
|
// icon with background-position
|
|
found.className = 'aciTreeIcon ' + options.icon[0];
|
|
found.style.backgroundPosition = options.icon[1] + 'px ' + options.icon[2] + 'px';
|
|
} else {
|
|
// only the CSS class name
|
|
found.className = 'aciTreeIcon ' + options.icon;
|
|
}
|
|
// remember this one
|
|
data.icon = options.icon;
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// remove item icon
|
|
// options.oldIcon will keep the old icon
|
|
removeIcon: function(item, options) {
|
|
options = this._options(options, 'iconremoved', 'removeiconfail', 'noticon', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeremoveicon', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var data = this.itemData(item);
|
|
// keep the old one
|
|
options.oldIcon = data.icon;
|
|
var parent = domApi.childrenByClass(item[0].firstChild, 'aciTreeItem');
|
|
var found = domApi.childrenByClass(parent, 'aciTreeIcon');
|
|
if (found) {
|
|
parent.removeChild(found);
|
|
// remember was removed
|
|
data.icon = null;
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if item has icon
|
|
hasIcon: function(item) {
|
|
return !!this.getIcon(item);
|
|
},
|
|
// get item icon
|
|
getIcon: function(item) {
|
|
var data = this.itemData(item);
|
|
return data ? data.icon : null;
|
|
},
|
|
// set item label
|
|
// `options.label` is the new label
|
|
// `options.oldLabel` will keep the old label
|
|
setLabel: function(item, options) {
|
|
options = this._options(options, 'labelset', 'labelfail', 'waslabel', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforelabel', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var data = this.itemData(item);
|
|
// keep the old one
|
|
options.oldLabel = data.label;
|
|
if (options.label == options.oldLabel) {
|
|
this._notify(item, options);
|
|
} else {
|
|
// set the label
|
|
domApi.childrenByClass(item[0].firstChild, 'aciTreeText').innerHTML = options.label;
|
|
// remember this one
|
|
data.label = options.label;
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// disable item
|
|
disable: function(item, options) {
|
|
options = this._options(options, 'disabled', 'disablefail', 'wasdisabled', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforedisable', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isDisabled(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
domApi.addClass(item[0], 'aciTreeDisabled');
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if item is disabled
|
|
isDisabled: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeDisabled');
|
|
},
|
|
// test if any of parents are disabled
|
|
isDisabledPath: function(item) {
|
|
return domApi.withClass(this.path(item).toArray(), 'aciTreeDisabled').length > 0;
|
|
},
|
|
// filter only the disabled items
|
|
disabled: function(items) {
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeDisabled'));
|
|
},
|
|
// enable item
|
|
enable: function(item, options) {
|
|
options = this._options(options, 'enabled', 'enablefail', 'wasenabled', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeenable', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isDisabled(item)) {
|
|
domApi.removeClass(item[0], 'aciTreeDisabled');
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if item is enabled
|
|
isEnabled: function(item) {
|
|
return item && !domApi.hasClass(item[0], 'aciTreeDisabled');
|
|
},
|
|
// test if all parents are enabled
|
|
isEnabledPath: function(item) {
|
|
return domApi.withClass(this.path(item).toArray(), 'aciTreeDisabled').length == 0;
|
|
},
|
|
// filter only the enabled items
|
|
enabled: function(items) {
|
|
return $(domApi.withClass(items.toArray(), null, 'aciTreeDisabled'));
|
|
},
|
|
// set item as hidden
|
|
hide: function(item, options) {
|
|
options = this._options(options, 'hidden', 'hidefail', 'washidden', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforehide', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isHidden(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
domApi.addRemoveClass(item[0], 'aciTreeHidden', 'aciTreeVisible');
|
|
// process children
|
|
domApi.addRemoveClass(this.children(item, true).toArray(), 'aciTreeHidden', 'aciTreeVisible');
|
|
// update item states
|
|
var parent = this.parent(item);
|
|
this._setFirstLast(parent.length ? parent : null, item);
|
|
this._setOddEven(item);
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if item is hidden
|
|
isHidden: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeHidden');
|
|
},
|
|
// test if any of parents are hidden
|
|
isHiddenPath: function(item) {
|
|
var parent = this.parent(item);
|
|
return parent[0] && domApi.hasClass(parent[0], 'aciTreeHidden');
|
|
},
|
|
// update hidden state
|
|
_updateHidden: function(item) {
|
|
if (this.isHiddenPath(item)) {
|
|
if (!this.isHidden(item)) {
|
|
domApi.addClass(item[0], 'aciTreeHidden');
|
|
this._updateVisible(item);
|
|
}
|
|
} else {
|
|
this._updateVisible(item);
|
|
}
|
|
},
|
|
// filter only the hidden items
|
|
hidden: function(items) {
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeHidden'));
|
|
},
|
|
// show hidden item
|
|
_showHidden: function(item) {
|
|
var parent = null;
|
|
this.path(item).add(item).each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (this.isHidden(item)) {
|
|
domApi.removeClass(item[0], 'aciTreeHidden');
|
|
if (this.isOpenPath(item) && (!parent || this.isOpen(parent))) {
|
|
domApi.addClass(item[0], 'aciTreeVisible');
|
|
}
|
|
// update item states
|
|
this._setFirstLast(parent, this._getFirstLast(parent));
|
|
}
|
|
parent = item;
|
|
}, true));
|
|
},
|
|
// show hidden item
|
|
show: function(item, options) {
|
|
options = this._options(options, 'shown', 'showfail', 'wasshown', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeshow', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isHidden(item)) {
|
|
this._showHidden(item);
|
|
var parent = this.topParent(item);
|
|
// update item states
|
|
this._setOddEven(parent.length ? parent : item);
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if item is open
|
|
isOpen: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeOpen');
|
|
},
|
|
// test if item is closed
|
|
isClosed: function(item) {
|
|
return item && !domApi.hasClass(item[0], 'aciTreeOpen');
|
|
},
|
|
// test if item has children
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
hasChildren: function(item, hidden) {
|
|
return this.children(item, false, hidden).length > 0;
|
|
},
|
|
// test if item has siblings
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
hasSiblings: function(item, hidden) {
|
|
return this.siblings(item, hidden).length > 0;
|
|
},
|
|
// test if item has another before
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
hasPrev: function(item, hidden) {
|
|
return this.prev(item, hidden).length > 0;
|
|
},
|
|
// test if item has another after
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
hasNext: function(item, hidden) {
|
|
return this.next(item, hidden).length > 0;
|
|
},
|
|
// get item siblings
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
siblings: function(item, hidden) {
|
|
return item ? $(domApi.children(item[0].parentNode.parentNode, false, function(node) {
|
|
return (node != item[0]) && (hidden || !this.hasClass(node, 'aciTreeHidden'));
|
|
})) : $([]);
|
|
},
|
|
// get previous item
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
prev: function(item, hidden) {
|
|
return item ? $(domApi.prev(item[0], hidden ? null : function(node) {
|
|
return !this.hasClass(node, 'aciTreeHidden');
|
|
})) : $([]);
|
|
},
|
|
// get next item
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
next: function(item, hidden) {
|
|
return item ? $(domApi.next(item[0], hidden ? null : function(node) {
|
|
return !this.hasClass(node, 'aciTreeHidden');
|
|
})) : $([]);
|
|
},
|
|
// get item level - starting from 0
|
|
// return -1 for invalid items
|
|
level: function(item) {
|
|
var level = -1;
|
|
if (item) {
|
|
var node = item[0];
|
|
while (domApi.hasClass(node, 'aciTreeLi')) {
|
|
node = node.parentNode.parentNode;
|
|
level++;
|
|
}
|
|
}
|
|
return level;
|
|
},
|
|
// get item ID
|
|
getId: function(item) {
|
|
var data = this.itemData(item);
|
|
return data ? data.id : null;
|
|
},
|
|
// get item data
|
|
itemData: function(item) {
|
|
return item ? item.data('itemData' + this._instance.nameSpace) : null;
|
|
},
|
|
// set item ID
|
|
// `options.id` is the new item ID
|
|
// `options.oldId` will keep the old ID
|
|
setId: function(item, options) {
|
|
options = this._options(options, 'idset', 'idfail', 'wasid', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeid', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var data = this.itemData(item);
|
|
// keep the old one
|
|
options.oldId = data.id;
|
|
if (options.id == options.oldId) {
|
|
this._notify(item, options);
|
|
} else {
|
|
// remember this one
|
|
data.id = options.id;
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// get item index - starting from #0
|
|
getIndex: function(item) {
|
|
if (item && item[0]) {
|
|
if (window.Array.prototype.indexOf) {
|
|
return window.Array.prototype.indexOf.call(item[0].parentNode.childNodes, item[0]);
|
|
} else {
|
|
var children = item[0].parentNode.childNodes;
|
|
for (var i = 0; i < children.length; i++) {
|
|
if (children[i] == item[0]) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// set item index - #0 based
|
|
// `options.index` is the new index
|
|
// `options.oldIndex` will keep the old index
|
|
setIndex: function(item, options) {
|
|
options = this._options(options, 'indexset', 'indexfail', 'wasindex', item);
|
|
if (this.isItem(item)) {
|
|
var oldIndex = this.getIndex(item);
|
|
var siblings = this.siblings(item);
|
|
if ((options.index != oldIndex) && !siblings.length) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeindex', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
// keep the old one
|
|
options.oldIndex = oldIndex;
|
|
if (options.index == oldIndex) {
|
|
this._notify(item, options);
|
|
} else {
|
|
// set the new index
|
|
if (options.index < 1) {
|
|
siblings.first().before(item);
|
|
} else if (options.index >= siblings.length) {
|
|
siblings.last().after(item);
|
|
} else {
|
|
siblings.eq(options.index).before(item);
|
|
}
|
|
var parent = this.parent(item);
|
|
// update item states
|
|
this._setFirstLast(parent.length ? parent : null, item.add([siblings[0], siblings.get(-1)]));
|
|
this._setOddEven(parent);
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// get item label
|
|
getLabel: function(item) {
|
|
var data = this.itemData(item);
|
|
return data ? data.label : null;
|
|
},
|
|
// test if is valid item
|
|
isItem: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeLi');
|
|
},
|
|
// item animation
|
|
// `state` if TRUE then show, FALSE then hide
|
|
// `unanimated` if TRUE then don't use animations
|
|
// `callback` function () to call at the end
|
|
_animate: function(item, state, unanimated, callback) {
|
|
if (!item) {
|
|
item = this._instance.jQuery;
|
|
}
|
|
if (!unanimated) {
|
|
// use the defined animation props
|
|
var setting = state ? this._instance.options.show : this._instance.options.hide;
|
|
if (setting) {
|
|
var ul = domApi.container(item[0]);
|
|
if (ul) {
|
|
// animate children container
|
|
$(ul).stop(true, true).animate(setting.props, {
|
|
duration: setting.duration,
|
|
easing: setting.easing,
|
|
complete: callback ? this.proxy(callback) : null
|
|
});
|
|
} else if (callback) {
|
|
callback.apply(this);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// use no animation
|
|
$(domApi.container(item[0])).stop(true, true).toggle(state);
|
|
if (callback) {
|
|
callback.apply(this);
|
|
}
|
|
},
|
|
// get first children of item
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
first: function(item, hidden) {
|
|
if (!item) {
|
|
item = this._instance.jQuery;
|
|
}
|
|
return $(domApi.firstChild(item[0], hidden ? null : function(node) {
|
|
return !this.hasClass(node, 'aciTreeHidden');
|
|
}));
|
|
},
|
|
// test if item is the first one for his parent
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
isFirst: function(item, hidden) {
|
|
if (item) {
|
|
var parent = domApi.parent(item[0]);
|
|
return this.first(parent ? $(parent) : null, hidden)[0] == item[0];
|
|
}
|
|
return false;
|
|
},
|
|
// get last children of item
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
last: function(item, hidden) {
|
|
if (!item) {
|
|
item = this._instance.jQuery;
|
|
}
|
|
return $(domApi.lastChild(item[0], hidden ? null : function(node) {
|
|
return !this.hasClass(node, 'aciTreeHidden');
|
|
}));
|
|
},
|
|
// test if item is the last one for his parent
|
|
// if `hidden` is TRUE then the hidden items will be considered too
|
|
isLast: function(item, hidden) {
|
|
if (item) {
|
|
var parent = domApi.parent(item[0]);
|
|
return this.last(parent ? $(parent) : null, hidden)[0] == item[0];
|
|
}
|
|
return false;
|
|
},
|
|
// test if item is busy/loading
|
|
isBusy: function(item) {
|
|
if (item) {
|
|
return domApi.hasClass(item[0], 'aciTreeLoad');
|
|
} else {
|
|
return this._instance.queue.busy();
|
|
}
|
|
},
|
|
// set loading state
|
|
_loading: function(item, state) {
|
|
if (item) {
|
|
domApi.toggleClass(item[0], 'aciTreeLoad', state);
|
|
if (state) {
|
|
item[0].firstChild.setAttribute('aria-busy', true);
|
|
} else {
|
|
item[0].firstChild.removeAttribute('aria-busy');
|
|
}
|
|
} else if (state) {
|
|
this._loader(state);
|
|
}
|
|
},
|
|
// show loader image
|
|
_loader: function(show) {
|
|
if (show || this.isBusy()) {
|
|
if (!this._private.loaderInterval) {
|
|
this._private.loaderInterval = window.setInterval(this.proxy(function() {
|
|
this._loader();
|
|
}), this._instance.options.loaderDelay);
|
|
}
|
|
domApi.addClass(this._instance.jQuery[0], 'aciTreeLoad');
|
|
window.clearTimeout(this._private.loaderHide);
|
|
this._private.loaderHide = window.setTimeout(this.proxy(function() {
|
|
domApi.removeClass(this._instance.jQuery[0], 'aciTreeLoad');
|
|
}), this._instance.options.loaderDelay * 2);
|
|
}
|
|
},
|
|
// test if parent has children
|
|
isChildren: function(parent, children) {
|
|
if (!parent) {
|
|
parent = this._instance.jQuery;
|
|
}
|
|
return children && (parent.has(children).length > 0);
|
|
},
|
|
// test if parent has immediate children
|
|
isImmediateChildren: function(parent, children) {
|
|
if (!parent) {
|
|
parent = this._instance.jQuery;
|
|
}
|
|
return children && parent.children('.aciTreeUl').children('.aciTreeLi').is(children);
|
|
},
|
|
// test if items share the same parent
|
|
sameParent: function(item1, item2) {
|
|
if (item1 && item2) {
|
|
var parent1 = this.parent(item1);
|
|
var parent2 = this.parent(item2);
|
|
return (!parent1.length && !parent2.length) || (parent1[0] == parent2[0]);
|
|
}
|
|
return false;
|
|
},
|
|
// test if items share the same top parent
|
|
sameTopParent: function(item1, item2) {
|
|
if (item1 && item2) {
|
|
var parent1 = this.topParent(item1);
|
|
var parent2 = this.topParent(item2);
|
|
return (!parent1.length && !parent2.length) || (parent1[0] == parent2[0]);
|
|
}
|
|
return false;
|
|
},
|
|
// return the updated item data
|
|
// `callback` function (item) called for each item
|
|
_serialize: function(item, callback) {
|
|
var data = this.itemData(item);
|
|
if (this.isInode(item)) {
|
|
data.inode = true;
|
|
if (this.wasLoad(item)) {
|
|
if (data.hasOwnProperty('open')) {
|
|
data.open = this.isOpen(item);
|
|
} else if (this.isOpen(item)) {
|
|
data.open = true;
|
|
}
|
|
data.branch = [];
|
|
this.children(item, false, true).each(this.proxy(function(element) {
|
|
var entry = this._serialize($(element), callback);
|
|
if (callback) {
|
|
entry = callback.call(this, $(element), {
|
|
}, entry);
|
|
} else {
|
|
entry = this._instance.options.serialize.call(this, $(element), {
|
|
}, entry);
|
|
}
|
|
if (entry) {
|
|
data.branch.push(entry);
|
|
}
|
|
}, true));
|
|
if (!data.branch.length) {
|
|
data.branch = null;
|
|
}
|
|
} else {
|
|
if (data.hasOwnProperty('open')) {
|
|
data.open = false;
|
|
}
|
|
if (data.hasOwnProperty('branch')) {
|
|
data.branch = null;
|
|
}
|
|
}
|
|
} else {
|
|
if (data.hasOwnProperty('inode')) {
|
|
data.inode = false;
|
|
}
|
|
if (data.hasOwnProperty('open')) {
|
|
data.open = null;
|
|
}
|
|
if (data.hasOwnProperty('branch')) {
|
|
data.branch = null;
|
|
}
|
|
}
|
|
if (data.hasOwnProperty('disabled')) {
|
|
data.disabled = this.isDisabled(item);
|
|
} else if (this.isDisabled(item)) {
|
|
data.disabled = true;
|
|
}
|
|
return data;
|
|
},
|
|
// return serialized data
|
|
// `callback` function (item, what, value) - see `aciTree.options.serialize`
|
|
serialize: function(item, what, callback) {
|
|
// override this to provide serialized data
|
|
if (typeof what == 'object') {
|
|
if (item) {
|
|
var data = this._serialize(item, callback);
|
|
if (callback) {
|
|
data = callback.call(this, item, {
|
|
}, data);
|
|
} else {
|
|
data = this._instance.options.serialize.call(this, item, {
|
|
}, data);
|
|
}
|
|
return data;
|
|
} else {
|
|
var list = [];
|
|
this.children(null, false, true).each(this.proxy(function(element) {
|
|
var data = this._serialize($(element), callback);
|
|
if (callback) {
|
|
data = callback.call(this, $(element), {
|
|
}, data);
|
|
} else {
|
|
data = this._instance.options.serialize.call(this, $(element), {
|
|
}, data);
|
|
}
|
|
if (data) {
|
|
list.push(data);
|
|
}
|
|
}, true));
|
|
return list;
|
|
}
|
|
}
|
|
return '';
|
|
},
|
|
// destroy the control
|
|
destroy: function(options) {
|
|
options = this._options(options);
|
|
// check if was init
|
|
if (!this.wasInit()) {
|
|
this._trigger(null, 'notinit', options);
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
// check if is locked
|
|
if (this.isLocked()) {
|
|
this._trigger(null, 'locked', options);
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
// a way to cancel the operation
|
|
if (!this._trigger(null, 'beforedestroy', options)) {
|
|
this._trigger(null, 'destroyfail', options);
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
this._private.locked = true;
|
|
this._instance.jQuery.addClass('aciTreeLoad').attr('aria-busy', true);
|
|
this._instance.queue.destroy();
|
|
this._destroyHook(false);
|
|
// unload the entire treeview
|
|
this.unload(null, this._inner(options, {
|
|
success: this.proxy(function() {
|
|
window.clearTimeout(this._private.loaderHide);
|
|
window.clearInterval(this._private.loaderInterval);
|
|
this._private.itemClone = {
|
|
};
|
|
this._destroyHook(true);
|
|
this._instance.jQuery.unbind(this._instance.nameSpace).off(this._instance.nameSpace, '.aciTreeButton').off(this._instance.nameSpace, '.aciTreeLine');
|
|
this._instance.jQuery.removeClass('aciTree' + this._instance.index + ' aciTreeLoad').removeAttr('role aria-busy');
|
|
this._private.locked = false;
|
|
// call the parent
|
|
this._super();
|
|
this._trigger(null, 'destroyed', options);
|
|
this._success(null, options);
|
|
}),
|
|
fail: function() {
|
|
this._instance.jQuery.removeClass('aciTreeLoad');
|
|
this._private.locked = false;
|
|
this._trigger(null, 'destroyfail', options);
|
|
this._fail(null, options);
|
|
}
|
|
}));
|
|
},
|
|
_destroyHook: function(unloaded) {
|
|
// override this to do extra destroy before/after unload
|
|
}
|
|
|
|
};
|
|
|
|
// extend the base aciPluginUi class and store into aciPluginClass.plugins
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.aciPluginUi.extend(aciTree_core, 'aciTreeCore');
|
|
|
|
// publish the plugin & the default options
|
|
aciPluginClass.publish('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* A few utility functions for aciTree.
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
// called when items need to be filtered, for each tree item
|
|
// return TRUE/FALSE to include/exclude items on filtering
|
|
filterHook: function(item, search, regexp) {
|
|
return search.length ? regexp.test(window.String(this.getLabel(item))) : true;
|
|
}
|
|
};
|
|
|
|
// aciTree utils extension
|
|
// adds item update option, branch processing, moving items & item swapping, item search by ID
|
|
|
|
var aciTree_utils = {
|
|
__extend: function() {
|
|
// add extra data
|
|
$.extend(this._instance, {
|
|
filter: new this._queue(this, this._instance.options.queue)
|
|
});
|
|
// stop queue until needed
|
|
this._instance.filter.destroy();
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// call the `callback` function (item) for each children of item
|
|
// when `load` is TRUE will also try to load nodes
|
|
branch: function(item, callback, load) {
|
|
var queue = this._instance.queue;
|
|
var process = this.proxy(function(item, callback, next) {
|
|
var child = next ? this.next(item) : this.first(item);
|
|
if (child.length) {
|
|
if (this.isInode(child)) {
|
|
if (this.wasLoad(child)) {
|
|
queue.push(function(complete) {
|
|
callback.call(this, child);
|
|
process(child, callback);
|
|
process(child, callback, true);
|
|
complete();
|
|
});
|
|
} else if (load) {
|
|
// load the item first
|
|
this.ajaxLoad(child, {
|
|
success: function() {
|
|
callback.call(this, child);
|
|
process(child, callback);
|
|
process(child, callback, true);
|
|
},
|
|
fail: function() {
|
|
process(child, callback, true);
|
|
}
|
|
});
|
|
} else {
|
|
queue.push(function(complete) {
|
|
callback.call(this, child);
|
|
process(child, callback, true);
|
|
complete();
|
|
});
|
|
}
|
|
} else {
|
|
queue.push(function(complete) {
|
|
callback.call(this, child);
|
|
process(child, callback, true);
|
|
complete();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
process(item, callback);
|
|
},
|
|
// swap two items (they can't be parent & children)
|
|
// `options.item1` & `options.item2` are the swapped items
|
|
swap: function(options) {
|
|
options = this._options(options, null, 'swapfail', null, null);
|
|
var item1 = options.item1;
|
|
var item2 = options.item2;
|
|
if (this.isItem(item1) && this.isItem(item2) && !this.isChildren(item1, item2) && !this.isChildren(item2, item1) && (item1[0] != item2[0])) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(null, 'beforeswap', options)) {
|
|
this._fail(null, options);
|
|
return;
|
|
}
|
|
var prev = this.prev(item1);
|
|
if (prev.length) {
|
|
if (item2[0] == prev[0]) {
|
|
item2.before(item1);
|
|
} else {
|
|
item1.insertAfter(item2);
|
|
item2.insertAfter(prev);
|
|
}
|
|
} else {
|
|
var next = this.next(item1);
|
|
if (next.length) {
|
|
if (item2[0] == next[0]) {
|
|
item2.after(item1);
|
|
} else {
|
|
item1.insertAfter(item2);
|
|
item2.insertBefore(next);
|
|
}
|
|
} else {
|
|
var parent = item1.parent();
|
|
item1.insertAfter(item2);
|
|
parent.append(item2);
|
|
}
|
|
}
|
|
// update item states
|
|
this._updateLevel(item1);
|
|
var parent = this.parent(item1);
|
|
this._setFirstLast(parent.length ? parent : null, item1);
|
|
this._updateHidden(item1);
|
|
this._updateLevel(item2);
|
|
parent = this.parent(item2);
|
|
this._setFirstLast(parent.length ? parent : null, item2);
|
|
this._updateHidden(item2);
|
|
this._setOddEven(item1.add(item2));
|
|
this._trigger(null, 'swapped', options);
|
|
this._success(null, options);
|
|
} else {
|
|
this._fail(null, options);
|
|
}
|
|
},
|
|
// update item level
|
|
_updateItemLevel: function(item, fromLevel, toLevel) {
|
|
domApi.addRemoveClass(item[0], 'aciTreeLevel' + toLevel, 'aciTreeLevel' + fromLevel);
|
|
var line = item[0].firstChild;
|
|
line.setAttribute('aria-level', toLevel + 1);
|
|
var entry = domApi.childrenByClass(line, 'aciTreeEntry');
|
|
if (fromLevel < toLevel) {
|
|
line = entry.parentNode;
|
|
var branch;
|
|
for (var i = fromLevel; i < toLevel; i++) {
|
|
branch = window.document.createElement('DIV');
|
|
line.appendChild(branch);
|
|
branch.className = 'aciTreeBranch aciTreeLevel' + i;
|
|
line = branch;
|
|
}
|
|
line.appendChild(entry);
|
|
} else {
|
|
var branch = entry;
|
|
for (var i = toLevel; i <= fromLevel; i++) {
|
|
branch = branch.parentNode;
|
|
}
|
|
branch.removeChild(branch.firstChild);
|
|
branch.appendChild(entry);
|
|
}
|
|
},
|
|
// update child level
|
|
_updateChildLevel: function(item, fromLevel, toLevel) {
|
|
this.children(item, false, true).each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
this._updateItemLevel(item, fromLevel, toLevel);
|
|
if (this.isInode(item)) {
|
|
this._updateChildLevel(item, fromLevel + 1, toLevel + 1);
|
|
}
|
|
}, true));
|
|
},
|
|
// update item level
|
|
_updateLevel: function(item) {
|
|
var level = this.level(item);
|
|
var found = window.parseInt(item.attr('class').match(/aciTreeLevel[0-9]+/)[0].match(/[0-9]+/));
|
|
if (level != found) {
|
|
this._updateItemLevel(item, found, level);
|
|
this._updateChildLevel(item, found + 1, level + 1);
|
|
}
|
|
},
|
|
// move item up
|
|
moveUp: function(item, options) {
|
|
options = this._options(options);
|
|
options.index = window.Math.max(this.getIndex(item) - 1, 0);
|
|
this.setIndex(item, options);
|
|
},
|
|
// move item down
|
|
moveDown: function(item, options) {
|
|
options = this._options(options);
|
|
options.index = window.Math.min(this.getIndex(item) + 1, this.siblings(item).length);
|
|
this.setIndex(item, options);
|
|
},
|
|
// move item in first position
|
|
moveFirst: function(item, options) {
|
|
options = this._options(options);
|
|
options.index = 0;
|
|
this.setIndex(item, options);
|
|
},
|
|
// move item in last position
|
|
moveLast: function(item, options) {
|
|
options = this._options(options);
|
|
options.index = this.siblings(item).length;
|
|
this.setIndex(item, options);
|
|
},
|
|
// move item before another (they can't be parent & children)
|
|
// `options.before` is the element before which the item will be moved
|
|
moveBefore: function(item, options) {
|
|
options = this._options(options, null, 'movefail', 'wasbefore', item);
|
|
var before = options.before;
|
|
if (this.isItem(item) && this.isItem(before) && !this.isChildren(item, before) && (item[0] != before[0])) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforemove', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.prev(before, true)[0] == item[0]) {
|
|
this._notify(item, options);
|
|
} else {
|
|
var parent = this.parent(item);
|
|
var prev = this.prev(item, true);
|
|
if (!prev.length) {
|
|
prev = parent.length ? parent : this.first();
|
|
}
|
|
item.insertBefore(before);
|
|
if (parent.length && !this.hasChildren(parent, true)) {
|
|
this.setLeaf(parent);
|
|
}
|
|
this._updateLevel(item);
|
|
// update item states
|
|
this._setFirstLast(parent.length ? parent : null);
|
|
parent = this.parent(item);
|
|
this._setFirstLast(parent.length ? parent : null, item.add(before));
|
|
this._updateHidden(item);
|
|
this._setOddEven(item.add(before).add(prev));
|
|
this._trigger(item, 'moved', options);
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// move item after another (they can't be parent & children)
|
|
// `options.after` is the element after which the item will be moved
|
|
moveAfter: function(item, options) {
|
|
options = this._options(options, null, 'movefail', 'wasafter', item);
|
|
var after = options.after;
|
|
if (this.isItem(item) && this.isItem(after) && !this.isChildren(item, after) && (item[0] != after[0])) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforemove', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.next(after, true)[0] == item[0]) {
|
|
this._notify(item, options);
|
|
} else {
|
|
var parent = this.parent(item);
|
|
var prev = this.prev(item, true);
|
|
if (!prev.length) {
|
|
prev = parent.length ? parent : this.first();
|
|
}
|
|
item.insertAfter(after);
|
|
if (parent.length && !this.hasChildren(parent, true)) {
|
|
this.setLeaf(parent);
|
|
}
|
|
this._updateLevel(item);
|
|
this._setFirstLast(parent.length ? parent : null);
|
|
parent = this.parent(item);
|
|
this._setFirstLast(parent.length ? parent : null, item.add(after));
|
|
this._updateHidden(item);
|
|
this._setOddEven(item.add(after).add(prev));
|
|
this._trigger(item, 'moved', options);
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// move item to be a child of another (they can't be parent & children and the targeted parent item must be empty)
|
|
// `options.parent` is the parent element on which the item will be added
|
|
asChild: function(item, options) {
|
|
options = this._options(options, null, 'childfail', null, item);
|
|
var parent = options.parent;
|
|
if (this.isItem(item) && this.isItem(parent) && !this.isChildren(item, parent) && !this.hasChildren(parent, true) && (item[0] != parent[0])) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforechild', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var process = function() {
|
|
var oldParent = this.parent(item);
|
|
var prev = this.prev(item);
|
|
if (!prev.length) {
|
|
prev = oldParent.length ? oldParent : this.first();
|
|
}
|
|
var container = this._createContainer(parent);
|
|
container.append(item);
|
|
if (oldParent.length && !this.hasChildren(oldParent, true)) {
|
|
// no more children
|
|
this.setLeaf(oldParent);
|
|
}
|
|
// update item states
|
|
this._updateLevel(item);
|
|
this._setFirstLast(oldParent.length ? oldParent : null);
|
|
this._setFirstLast(parent.length ? parent : null, item);
|
|
this._updateHidden(item);
|
|
this._setOddEven(item.add(prev));
|
|
this._trigger(item, 'childset', options);
|
|
this._success(item, options);
|
|
};
|
|
if (this.isInode(parent)) {
|
|
process.apply(this);
|
|
} else {
|
|
// set as inode first
|
|
this.setInode(parent, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// search a `path` ID from a parent
|
|
_search: function(parent, pathId) {
|
|
var items = this.children(parent);
|
|
var item, id, length, found, exact = false;
|
|
for (var i = 0, size = items.length; i < size; i++) {
|
|
item = items.eq(i);
|
|
id = window.String(this.getId(item));
|
|
length = id.length;
|
|
if (length) {
|
|
if (id == pathId.substr(0, length)) {
|
|
found = item;
|
|
exact = pathId.length == length;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (found) {
|
|
if (!exact) {
|
|
// try to search children
|
|
var child = this._search(found, pathId);
|
|
if (child) {
|
|
return child;
|
|
}
|
|
}
|
|
return {
|
|
item: found,
|
|
exact: exact
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
// search items by ID
|
|
// `options.id` is the ID to search for
|
|
// if `path` is TRUE then the search will be more optimized
|
|
// and reduced to the first branch that matches the ID
|
|
// but the ID must be set like a path otherwise will not work
|
|
// if `load` is TRUE will also try to load nodes (works only when `path` is TRUE)
|
|
searchId: function(path, load, options) {
|
|
options = this._options(options);
|
|
var id = options.id;
|
|
if (path) {
|
|
if (load) {
|
|
var process = this.proxy(function(item) {
|
|
var found = this._search(item, id);
|
|
if (found) {
|
|
if (found.exact) {
|
|
this._success(found.item, options);
|
|
} else {
|
|
if (this.wasLoad(found.item)) {
|
|
this._fail(item, options);
|
|
} else {
|
|
// load the item
|
|
this.ajaxLoad(found.item, this._inner(options, {
|
|
success: function() {
|
|
process(found.item);
|
|
},
|
|
fail: options.fail
|
|
}));
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
});
|
|
process();
|
|
} else {
|
|
var found = this._search(null, id);
|
|
if (found && found.exact) {
|
|
this._success(found.item, options);
|
|
} else {
|
|
this._fail(null, options);
|
|
}
|
|
}
|
|
} else {
|
|
var found = $();
|
|
this._instance.jQuery.find('.aciTreeLi').each(this.proxy(function(element) {
|
|
if (id == this.getId($(element))) {
|
|
found = $(element);
|
|
return false;
|
|
}
|
|
}, true));
|
|
if (found.length) {
|
|
this._success(found, options);
|
|
} else {
|
|
this._fail(null, options);
|
|
}
|
|
}
|
|
},
|
|
// search nodes by ID or custom property starting from item
|
|
// `options.search` is the value to be searched
|
|
// `options.load` if TRUE will try to load nodes
|
|
// `options.callback` function (item, search) return TRUE for the custom match
|
|
// `options.results` will keep the search results
|
|
search: function(item, options) {
|
|
var results = [];
|
|
options = this._options(options);
|
|
var task = new this._task(new this._queue(this, this._instance.options.queue), function(complete) {
|
|
// run this at the end
|
|
if (results.length) {
|
|
options.results = $(results);
|
|
this._success($(results[0]), options);
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
complete();
|
|
});
|
|
var children = this.proxy(function(item) {
|
|
this.children(item, false, true).each(this.proxy(function(element) {
|
|
if (options.callback) {
|
|
// custom search
|
|
var match = options.callback.call(this, $(element), options.search);
|
|
if (match) {
|
|
results.push(element);
|
|
} else if (match === null) {
|
|
// skip childrens
|
|
return;
|
|
}
|
|
} else if (this.getId($(element)) == options.search) {
|
|
// default ID match
|
|
results.push(element);
|
|
}
|
|
if (this.isInode($(element))) {
|
|
// process children
|
|
task.push(function(complete) {
|
|
search($(element));
|
|
complete();
|
|
});
|
|
}
|
|
}, true));
|
|
});
|
|
var search = this.proxy(function(item) {
|
|
if (this.wasLoad(item)) {
|
|
// process children
|
|
task.push(function(complete) {
|
|
children(item);
|
|
complete();
|
|
});
|
|
} else if (options.load) {
|
|
task.push(function(complete) {
|
|
// load the item first
|
|
this.ajaxLoad(item, {
|
|
success: function() {
|
|
children(item);
|
|
complete();
|
|
},
|
|
fail: complete
|
|
});
|
|
});
|
|
}
|
|
});
|
|
// run the search
|
|
task.push(function(complete) {
|
|
search(item);
|
|
complete();
|
|
});
|
|
},
|
|
// search node by a list of IDs starting from item
|
|
// `options.path` is a list of IDs to be searched - the path to the node
|
|
// `options.load` if TRUE will try to load nodes
|
|
searchPath: function(item, options) {
|
|
options = this._options(options);
|
|
var path = options.path;
|
|
var search = this.proxy(function(item, id) {
|
|
this.search(item, {
|
|
success: function(item) {
|
|
if (path.length) {
|
|
search(item, path.shift());
|
|
} else {
|
|
this._success(item, options);
|
|
}
|
|
},
|
|
fail: function() {
|
|
this._fail(item, options);
|
|
},
|
|
search: id,
|
|
load: options.load,
|
|
callback: function(item, search) {
|
|
// prevent drill-down
|
|
return (this.getId(item) == search) ? true : null;
|
|
}
|
|
});
|
|
});
|
|
search(item, path.shift());
|
|
},
|
|
// get item path IDs starting from the top parent (ROOT)
|
|
// when `reverse` is TRUE returns the IDs in reverse order
|
|
pathId: function(item, reverse) {
|
|
var path = this.path(item, reverse), id = [];
|
|
path.each(this.proxy(function(element) {
|
|
id.push(this.getId($(element)));
|
|
}, true));
|
|
return id;
|
|
},
|
|
// escape string and return RegExp
|
|
_regexp: function(search) {
|
|
return new window.RegExp(window.String(search).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').replace(/\x08/g, '\\x08'), 'i');
|
|
},
|
|
// filter the tree items based on search criteria
|
|
// `options.search` is the keyword
|
|
// `options.first` will be the first matched item (if any)
|
|
filter: function(item, options) {
|
|
options = this._options(options, null, 'filterfail', null, item);
|
|
if (!item || this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforefilter', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var search = window.String(options.search);
|
|
var regexp = this._regexp(search);
|
|
var first = null;
|
|
this._instance.filter.init();
|
|
var task = new this._task(this._instance.filter, function(complete) {
|
|
// run this at the end
|
|
this._instance.filter.destroy();
|
|
options.first = first;
|
|
this._setOddEven();
|
|
this._trigger(item, 'filtered', options);
|
|
this._success(item, options);
|
|
complete();
|
|
});
|
|
// process children
|
|
var process = this.proxy(function(parent) {
|
|
var children = this.children(parent, false, true);
|
|
var found = false;
|
|
children.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (this._instance.options.filterHook.call(this, item, search, regexp)) {
|
|
if (!first) {
|
|
first = item;
|
|
}
|
|
found = true;
|
|
domApi.removeClass(item[0], 'aciTreeHidden');
|
|
} else {
|
|
domApi.addRemoveClass(item[0], 'aciTreeHidden', 'aciTreeVisible');
|
|
}
|
|
if (this.isInode(item)) {
|
|
// continue with the children
|
|
task.push(function(complete) {
|
|
process(item);
|
|
complete();
|
|
});
|
|
}
|
|
}, true));
|
|
if (found) {
|
|
// update item states
|
|
if (parent && this.isHidden(parent)) {
|
|
this._showHidden(parent);
|
|
}
|
|
if (!parent || (this.isOpenPath(parent) && this.isOpen(parent))) {
|
|
children.not('.aciTreeHidden').addClass('aciTreeVisible');
|
|
}
|
|
this._setFirstLast(parent, this._getFirstLast(parent));
|
|
}
|
|
});
|
|
task.push(function(complete) {
|
|
process(item);
|
|
complete();
|
|
});
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// call the `callback` function (item) for the first item
|
|
_firstAll: function(callback) {
|
|
callback.call(this, this.first());
|
|
},
|
|
// call the `callback` function (item) for the last item
|
|
// when `load` is TRUE will also try to load nodes
|
|
_lastAll: function(item, callback, load) {
|
|
if (item) {
|
|
if (this.isInode(item)) {
|
|
if (this.wasLoad(item)) {
|
|
this._lastAll(this.last(item), callback, load);
|
|
return;
|
|
} else if (load) {
|
|
this.ajaxLoad(item, {
|
|
success: function() {
|
|
this._lastAll(this.last(item), callback, load);
|
|
},
|
|
fail: function() {
|
|
callback.call(this, item);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
callback.call(this, item);
|
|
} else {
|
|
callback.call(this, this.last());
|
|
}
|
|
},
|
|
// call the `callback` function (item) for the next item from tree
|
|
// when `load` is TRUE will also try to load nodes
|
|
_nextAll: function(item, callback, load) {
|
|
if (item) {
|
|
if (this.isInode(item)) {
|
|
if (this.wasLoad(item)) {
|
|
callback.call(this, this.first(item));
|
|
return;
|
|
} else if (load) {
|
|
this.ajaxLoad(item, {
|
|
success: function() {
|
|
callback.call(this, this.first(item));
|
|
},
|
|
fail: function() {
|
|
this._nextAll(item, callback, load);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
var next = this.next(item);
|
|
if (next.length) {
|
|
callback.call(this, next);
|
|
} else {
|
|
// search next by parents
|
|
var search = this.proxy(function(item) {
|
|
var parent = this.parent(item);
|
|
if (parent.length) {
|
|
var next = this.next(parent);
|
|
if (next.length) {
|
|
return next;
|
|
} else {
|
|
return search(parent);
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
callback.call(this, search(item));
|
|
}
|
|
} else {
|
|
callback.call(this, this.first());
|
|
}
|
|
},
|
|
// call the `callback` function (item) for the previous item from tree
|
|
// when `load` is TRUE will also try to load nodes
|
|
_prevAll: function(item, callback, load) {
|
|
if (item) {
|
|
var prev = this.prev(item);
|
|
if (prev.length) {
|
|
if (this.isInode(prev)) {
|
|
this._lastAll(prev, callback, load);
|
|
} else {
|
|
callback.call(this, prev);
|
|
}
|
|
} else {
|
|
var parent = this.parent(item);
|
|
callback.call(this, parent.length ? parent : null);
|
|
}
|
|
} else {
|
|
callback.call(this, this.last());
|
|
}
|
|
},
|
|
// call the `callback` function (item) with the previous found item based on search criteria
|
|
// `search` is the keyword
|
|
prevMatch: function(item, search, callback) {
|
|
var regexp = this._regexp(search);
|
|
this._instance.filter.init();
|
|
var task = new this._task(this._instance.filter, function(complete) {
|
|
this._instance.filter.destroy();
|
|
complete();
|
|
});
|
|
var process = function(item) {
|
|
task.push(function(complete) {
|
|
this._prevAll(item, function(item) {
|
|
if (item) {
|
|
if (this._instance.options.filterHook.call(this, item, search, regexp)) {
|
|
callback.call(this, item);
|
|
} else {
|
|
process(item);
|
|
}
|
|
} else {
|
|
callback.call(this, null);
|
|
}
|
|
complete();
|
|
});
|
|
});
|
|
};
|
|
process(this.isItem(item) ? item : null);
|
|
},
|
|
// call the `callback` function (item) with the next found item based on search criteria
|
|
// `search` is the keyword
|
|
nextMatch: function(item, search, callback) {
|
|
var regexp = this._regexp(search);
|
|
this._instance.filter.init();
|
|
var task = new this._task(this._instance.filter, function(complete) {
|
|
this._instance.filter.destroy();
|
|
complete();
|
|
});
|
|
var process = function(item) {
|
|
task.push(function(complete) {
|
|
this._nextAll(item, function(item) {
|
|
if (item) {
|
|
if (this._instance.options.filterHook.call(this, item, search, regexp)) {
|
|
callback.call(this, item);
|
|
} else {
|
|
process(item);
|
|
}
|
|
} else {
|
|
callback.call(this, null);
|
|
}
|
|
complete();
|
|
});
|
|
});
|
|
};
|
|
process(this.isItem(item) ? item : null);
|
|
}
|
|
|
|
};
|
|
|
|
// extend the base aciTree class and add the utils stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_utils, 'aciTreeUtils');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds item selection/keyboard navigation to aciTree and need to
|
|
* be always included if you care about accessibility.
|
|
*
|
|
* There is an extra property for the item data:
|
|
*
|
|
* {
|
|
* ...
|
|
* selected: false, // TRUE means the item will be selected
|
|
* ...
|
|
* }
|
|
*
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
selectable: true, // if TRUE then one item can be selected (and the tree navigation with the keyboard will be enabled)
|
|
multiSelectable: false, // if TRUE then multiple items can be selected at a time
|
|
// the 'tabIndex' attribute need to be >= 0 set on the tree container (by default will be set to 0)
|
|
fullRow: false, // if TRUE then the selection will be made on the entire row (the CSS should reflect this)
|
|
textSelection: false // if FALSE then the item text can't be selected
|
|
};
|
|
|
|
// aciTree selectable extension
|
|
// adds item selection & keyboard navigation (left/right, up/down, pageup/pagedown, home/end, space, enter, escape)
|
|
// dblclick also toggles the item
|
|
|
|
var aciTree_selectable = {
|
|
__extend: function() {
|
|
// add extra data
|
|
$.extend(this._instance, {
|
|
focus: false
|
|
});
|
|
$.extend(this._private, {
|
|
blurTimeout: null,
|
|
spinPoint: null // the selected item to operate against when using the shift key with selection
|
|
});
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// test if has focus
|
|
hasFocus: function() {
|
|
return this._instance.focus;
|
|
},
|
|
// init selectable
|
|
_selectableInit: function() {
|
|
if (this._instance.jQuery.attr('tabindex') === undefined) {
|
|
// ensure the tree can get focus
|
|
this._instance.jQuery.attr('tabindex', 0);
|
|
}
|
|
if (!this._instance.options.textSelection) {
|
|
// disable text selection
|
|
this._selectable(false);
|
|
}
|
|
this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) {
|
|
switch (eventName) {
|
|
case 'closed':
|
|
var focused = api.focused();
|
|
if (api.isChildren(item, focused)) {
|
|
// move focus to parent on close
|
|
api._focusOne(item);
|
|
}
|
|
// deselect children on parent close
|
|
api.children(item, true).each(api.proxy(function(element) {
|
|
var item = $(element);
|
|
if (this.isSelected(item)) {
|
|
this.deselect(item);
|
|
}
|
|
}, true));
|
|
break;
|
|
}
|
|
}).bind('focusin' + this._private.nameSpace, this.proxy(function() {
|
|
// handle tree focus
|
|
window.clearTimeout(this._private.blurTimeout);
|
|
if (!this.hasFocus()) {
|
|
this._instance.focus = true;
|
|
domApi.addClass(this._instance.jQuery[0], 'aciTreeFocus');
|
|
this._trigger(null, 'focused');
|
|
}
|
|
})).bind('focusout' + this._private.nameSpace, this.proxy(function() {
|
|
// handle tree focus
|
|
window.clearTimeout(this._private.blurTimeout);
|
|
this._private.blurTimeout = window.setTimeout(this.proxy(function() {
|
|
if (this.hasFocus()) {
|
|
this._instance.focus = false;
|
|
domApi.removeClass(this._instance.jQuery[0], 'aciTreeFocus');
|
|
this._trigger(null, 'blurred');
|
|
}
|
|
}), 10);
|
|
})).bind('keydown' + this._private.nameSpace, this.proxy(function(e) {
|
|
if (!this.hasFocus()) {
|
|
// do not handle if we do not have focus
|
|
return;
|
|
}
|
|
var focused = this.focused();
|
|
if (focused.length && this.isBusy(focused)) {
|
|
// skip when busy
|
|
return false;
|
|
}
|
|
var item = $([]);
|
|
switch (e.which) {
|
|
case 65: // aA
|
|
if (this._instance.options.multiSelectable && e.ctrlKey) {
|
|
// select all visible items
|
|
var select = this.visible(this.enabled(this.children(null, true))).not(this.selected());
|
|
select.each(this.proxy(function(element) {
|
|
this.select($(element), {
|
|
focus: false
|
|
});
|
|
}, true));
|
|
if (!this.focused().length) {
|
|
// ensure one item has focus
|
|
this._focusOne(this.visible(select, true).first());
|
|
}
|
|
// prevent default action
|
|
e.preventDefault();
|
|
}
|
|
break;
|
|
case 38: // up
|
|
item = focused.length ? this._prev(focused) : this.first();
|
|
break;
|
|
case 40: // down
|
|
item = focused.length ? this._next(focused) : this.first();
|
|
break;
|
|
case 37: // left
|
|
if (focused.length) {
|
|
if (this.isOpen(focused)) {
|
|
item = focused;
|
|
// close the item
|
|
this.close(focused, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
} else {
|
|
item = this.parent(focused);
|
|
}
|
|
} else {
|
|
item = this._first();
|
|
}
|
|
break;
|
|
case 39: // right
|
|
if (focused.length) {
|
|
if (this.isInode(focused) && this.isClosed(focused)) {
|
|
item = focused;
|
|
// open the item
|
|
this.open(focused, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
} else {
|
|
item = this.first(focused);
|
|
}
|
|
} else {
|
|
item = this._first();
|
|
}
|
|
break;
|
|
case 33: // pgup
|
|
item = focused.length ? this._prevPage(focused) : this._first();
|
|
break;
|
|
case 34: // pgdown
|
|
item = focused.length ? this._nextPage(focused) : this._first();
|
|
break;
|
|
case 36: // home
|
|
item = this._first();
|
|
break;
|
|
case 35: // end
|
|
item = this._last();
|
|
break;
|
|
case 13: // enter
|
|
case 107: // numpad [+]
|
|
item = focused;
|
|
if (this.isInode(focused) && this.isClosed(focused)) {
|
|
// open the item
|
|
this.open(focused, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
}
|
|
break;
|
|
case 27: // escape
|
|
case 109: // numpad [-]
|
|
item = focused;
|
|
if (this.isOpen(focused)) {
|
|
// close the item
|
|
this.close(focused, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
}
|
|
if (e.which == 27) {
|
|
// prevent default action on ESC
|
|
e.preventDefault();
|
|
}
|
|
break;
|
|
case 32: // space
|
|
item = focused;
|
|
if (this.isInode(focused) && !e.ctrlKey) {
|
|
// toggle the item
|
|
this.toggle(focused, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
}
|
|
// prevent page scroll
|
|
e.preventDefault();
|
|
break;
|
|
case 106: // numpad [*]
|
|
item = focused;
|
|
if (this.isInode(focused)) {
|
|
// open all children
|
|
this.open(focused, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: true,
|
|
unique: this._instance.options.unique
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
if (item.length) {
|
|
if (this._instance.options.multiSelectable && !e.ctrlKey && !e.shiftKey) {
|
|
// unselect others
|
|
this._unselect(this.selected().not(item));
|
|
}
|
|
if (!this.isVisible(item)) {
|
|
// bring it into view
|
|
this.setVisible(item);
|
|
}
|
|
if (e.ctrlKey) {
|
|
if ((e.which == 32) && this.isEnabled(item)) { // space
|
|
if (this.isSelected(item)) {
|
|
this.deselect(item);
|
|
} else {
|
|
this.select(item);
|
|
}
|
|
// remember for later
|
|
this._private.spinPoint = item;
|
|
} else {
|
|
this._focusOne(item);
|
|
}
|
|
} else if (e.shiftKey) {
|
|
this._shiftSelect(item);
|
|
} else {
|
|
if (!this.isSelected(item) && this.isEnabled(item)) {
|
|
this.select(item);
|
|
} else {
|
|
this._focusOne(item);
|
|
}
|
|
// remember for later
|
|
this._private.spinPoint = item;
|
|
}
|
|
return false;
|
|
}
|
|
}));
|
|
this._fullRow(this._instance.options.fullRow);
|
|
this._multiSelectable(this._instance.options.multiSelectable);
|
|
},
|
|
// change full row mode
|
|
_fullRow: function(state) {
|
|
this._instance.jQuery.off(this._private.nameSpace, '.aciTreeLine,.aciTreeItem').off(this._private.nameSpace, '.aciTreeItem');
|
|
this._instance.jQuery.on('mousedown' + this._private.nameSpace + ' click' + this._private.nameSpace, state ? '.aciTreeLine,.aciTreeItem' : '.aciTreeItem', this.proxy(function(e) {
|
|
var item = this.itemFrom(e.target);
|
|
if (!this.isVisible(item)) {
|
|
this.setVisible(item);
|
|
}
|
|
if (e.ctrlKey) {
|
|
if (e.type == 'click') {
|
|
if (this.isEnabled(item)) {
|
|
// (de)select item
|
|
if (this.isSelected(item)) {
|
|
this.deselect(item);
|
|
this._focusOne(item);
|
|
} else {
|
|
this.select(item);
|
|
}
|
|
} else {
|
|
this._focusOne(item);
|
|
}
|
|
}
|
|
} else if (this._instance.options.multiSelectable && e.shiftKey) {
|
|
this._shiftSelect(item);
|
|
} else {
|
|
if (this._instance.options.multiSelectable && (!this.isSelected(item) || (e.type == 'click'))) {
|
|
// deselect all other (keep the old focus)
|
|
this._unselect(this.selected().not(item));
|
|
}
|
|
this._selectOne(item);
|
|
}
|
|
if (!e.shiftKey) {
|
|
this._private.spinPoint = item;
|
|
}
|
|
})).on('dblclick' + this._private.nameSpace, state ? '.aciTreeLine,.aciTreeItem' : '.aciTreeItem', this.proxy(function(e) {
|
|
var item = this.itemFrom(e.target);
|
|
if (this.isInode(item)) {
|
|
// toggle the item
|
|
this.toggle(item, {
|
|
collapse: this._instance.options.collapse,
|
|
expand: this._instance.options.expand,
|
|
unique: this._instance.options.unique
|
|
});
|
|
return false;
|
|
}
|
|
}));
|
|
if (state) {
|
|
domApi.addClass(this._instance.jQuery[0], 'aciTreeFullRow');
|
|
} else {
|
|
domApi.removeClass(this._instance.jQuery[0], 'aciTreeFullRow');
|
|
}
|
|
},
|
|
// change selection mode
|
|
_multiSelectable: function(state) {
|
|
if (state) {
|
|
this._instance.jQuery.attr('aria-multiselectable', true);
|
|
} else {
|
|
var focused = this.focused();
|
|
this._unselect(this.selected().not(focused));
|
|
this._instance.jQuery.removeAttr('aria-multiselectable');
|
|
}
|
|
},
|
|
// process `shift` key selection
|
|
_shiftSelect: function(item) {
|
|
var spinPoint = this._private.spinPoint;
|
|
if (!spinPoint || !$.contains(this._instance.jQuery[0], spinPoint[0]) || !this.isOpenPath(spinPoint)) {
|
|
spinPoint = this.focused();
|
|
}
|
|
if (spinPoint.length) {
|
|
// select a range of items
|
|
var select = [item[0]], start = spinPoint[0], found = false, stop = item[0];
|
|
var visible = this.visible(this.children(null, true));
|
|
visible.each(this.proxy(function(element) {
|
|
// find what items to select
|
|
if (found) {
|
|
if (this.isEnabled($(element))) {
|
|
select.push(element);
|
|
}
|
|
if ((element == start) || (element == stop)) {
|
|
return false;
|
|
}
|
|
} else if ((element == start) || (element == stop)) {
|
|
if (this.isEnabled($(element))) {
|
|
select.push(element);
|
|
}
|
|
if ((element == start) && (element == stop)) {
|
|
return false;
|
|
}
|
|
found = true;
|
|
}
|
|
}, true));
|
|
this._unselect(this.selected().not(select));
|
|
// select the items
|
|
$(select).not(item).each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (!this.isSelected(item)) {
|
|
// select item (keep the old focus)
|
|
this.select(item, {
|
|
focus: false
|
|
});
|
|
}
|
|
}, true));
|
|
}
|
|
this._selectOne(item);
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extSelectable()) {
|
|
this._selectableInit();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// override `_itemHook`
|
|
_itemHook: function(parent, item, itemData, level) {
|
|
if (this.extSelectable() && itemData.selected) {
|
|
this._selectableDOM.select(item, true);
|
|
}
|
|
// call the parent
|
|
this._super(parent, item, itemData, level);
|
|
},
|
|
// low level DOM functions
|
|
_selectableDOM: {
|
|
// (de)select one or more items
|
|
select: function(items, state) {
|
|
if (state) {
|
|
domApi.addListClass(items.toArray(), 'aciTreeSelected', function(node) {
|
|
node.firstChild.setAttribute('aria-selected', true);
|
|
});
|
|
} else {
|
|
domApi.removeListClass(items.toArray(), 'aciTreeSelected', function(node) {
|
|
node.firstChild.setAttribute('aria-selected', false);
|
|
});
|
|
}
|
|
},
|
|
// focus one item, unfocus one or more items
|
|
focus: function(items, state) {
|
|
if (state) {
|
|
domApi.addClass(items[0], 'aciTreeFocus');
|
|
items[0].firstChild.focus();
|
|
} else {
|
|
domApi.removeListClass(items.toArray(), 'aciTreeFocus');
|
|
}
|
|
}
|
|
},
|
|
// make element (un)selectable
|
|
_selectable: function(state) {
|
|
if (state) {
|
|
this._instance.jQuery.css({
|
|
'-webkit-user-select': 'text',
|
|
'-moz-user-select': 'text',
|
|
'-ms-user-select': 'text',
|
|
'-o-user-select': 'text',
|
|
'user-select': 'text'
|
|
}).attr({
|
|
'unselectable': null,
|
|
'onselectstart': null
|
|
}).unbind('selectstart' + this._private.nameSpace);
|
|
} else {
|
|
this._instance.jQuery.css({
|
|
'-webkit-user-select': 'none',
|
|
'-moz-user-select': '-moz-none',
|
|
'-ms-user-select': 'none',
|
|
'-o-user-select': 'none',
|
|
'user-select': 'none'
|
|
}).attr({
|
|
'unselectable': 'on',
|
|
'onselectstart': 'return false'
|
|
}).bind('selectstart' + this._private.nameSpace, function(e) {
|
|
if (!$(e.target).is('input,textarea')) {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
// get first visible item
|
|
_first: function() {
|
|
return $(domApi.first(this._instance.jQuery[0], function(node) {
|
|
return this.hasClass(node, 'aciTreeVisible') ? true : null;
|
|
}));
|
|
},
|
|
// get last visible item
|
|
_last: function() {
|
|
return $(domApi.last(this._instance.jQuery[0], function(node) {
|
|
return this.hasClass(node, 'aciTreeVisible') ? true : null;
|
|
}));
|
|
},
|
|
// get previous visible starting with item
|
|
_prev: function(item) {
|
|
return $(domApi.prevAll(item[0], function(node) {
|
|
return this.hasClass(node, 'aciTreeVisible') ? true : null;
|
|
}));
|
|
},
|
|
// get next visible starting with item
|
|
_next: function(item) {
|
|
return $(domApi.nextAll(item[0], function(node) {
|
|
return this.hasClass(node, 'aciTreeVisible') ? true : null;
|
|
}));
|
|
},
|
|
// get previous page starting with item
|
|
_prevPage: function(item) {
|
|
var space = this._instance.jQuery.height();
|
|
var now = item[0].firstChild.offsetHeight;
|
|
var prev = item, last = $();
|
|
while (now < space) {
|
|
prev = this._prev(prev);
|
|
if (prev[0]) {
|
|
now += prev[0].firstChild.offsetHeight;
|
|
last = prev;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return last;
|
|
},
|
|
// get next page starting with item
|
|
_nextPage: function(item) {
|
|
var space = this._instance.jQuery.height();
|
|
var now = item[0].firstChild.offsetHeight;
|
|
var next = item, last = $();
|
|
while (now < space) {
|
|
next = this._next(next);
|
|
if (next[0]) {
|
|
now += next[0].firstChild.offsetHeight;
|
|
last = next;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return last;
|
|
},
|
|
// select one item
|
|
_selectOne: function(item) {
|
|
if (this.isSelected(item)) {
|
|
this._focusOne(item);
|
|
} else {
|
|
if (this.isEnabled(item)) {
|
|
// select the item
|
|
this.select(item);
|
|
} else {
|
|
this._focusOne(item);
|
|
}
|
|
}
|
|
},
|
|
// unselect the items
|
|
_unselect: function(items) {
|
|
items.each(this.proxy(function(element) {
|
|
this.deselect($(element));
|
|
}, true));
|
|
},
|
|
// focus one item
|
|
_focusOne: function(item) {
|
|
if (!this._instance.options.multiSelectable) {
|
|
this._unselect(this.selected().not(item));
|
|
}
|
|
if (!this.isFocused(item)) {
|
|
this.focus(item);
|
|
}
|
|
},
|
|
// select item
|
|
// `options.focus` when set to FALSE will not set the focus
|
|
// `options.oldSelected` will keep the old selected items
|
|
select: function(item, options) {
|
|
options = this._options(options, 'selected', 'selectfail', 'wasselected', item);
|
|
if (this.extSelectable() && this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeselect', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
// keep the old ones
|
|
options.oldSelected = this.selected();
|
|
if (!this._instance.options.multiSelectable) {
|
|
// deselect all other
|
|
var unselect = options.oldSelected.not(item);
|
|
this._selectableDOM.select(unselect, false);
|
|
unselect.each(this.proxy(function(element) {
|
|
this._trigger($(element), 'deselected', options);
|
|
}, true));
|
|
}
|
|
if (this.isSelected(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
this._selectableDOM.select(item, true);
|
|
this._success(item, options);
|
|
}
|
|
// process focus
|
|
if ((options.focus === undefined) || options.focus) {
|
|
if (!this.isFocused(item) || options.focus) {
|
|
this.focus(item, this._inner(options));
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// deselect item
|
|
deselect: function(item, options) {
|
|
options = this._options(options, 'deselected', 'deselectfail', 'notselected', item);
|
|
if (this.extSelectable() && this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforedeselect', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isSelected(item)) {
|
|
this._selectableDOM.select(item, false);
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// set `virtual` focus
|
|
// `options.oldFocused` will keep the old focused item
|
|
focus: function(item, options) {
|
|
options = this._options(options, 'focus', 'focusfail', 'wasfocused', item);
|
|
if (this.extSelectable() && this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforefocus', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
// keep the old ones
|
|
options.oldFocused = this.focused();
|
|
// blur all other
|
|
var unfocus = options.oldFocused.not(item);
|
|
this._selectableDOM.focus(unfocus, false);
|
|
// unfocus all others
|
|
unfocus.each(this.proxy(function(element) {
|
|
this._trigger($(element), 'blur', options);
|
|
}, true));
|
|
if (this.isFocused(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
this._selectableDOM.focus(item, true);
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// remove `virtual` focus
|
|
blur: function(item, options) {
|
|
options = this._options(options, 'blur', 'blurfail', 'notfocused', item);
|
|
if (this.extSelectable() && this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeblur', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isFocused(item)) {
|
|
this._selectableDOM.focus(item, false);
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// get selected items
|
|
selected: function() {
|
|
return this._instance.jQuery.find('.aciTreeSelected');
|
|
},
|
|
// override `_serialize`
|
|
_serialize: function(item, callback) {
|
|
// call the parent
|
|
var data = this._super(item, callback);
|
|
if (data && this.extSelectable()) {
|
|
if (data.hasOwnProperty('selected')) {
|
|
data.selected = this.isSelected(item);
|
|
} else if (this.isSelected(item)) {
|
|
data.selected = true;
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
// test if item is selected
|
|
isSelected: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeSelected');
|
|
},
|
|
// return the focused item
|
|
focused: function() {
|
|
return this._instance.jQuery.find('.aciTreeFocus');
|
|
},
|
|
// test if item is focused
|
|
isFocused: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeFocus');
|
|
},
|
|
// test if selectable is enabled
|
|
extSelectable: function() {
|
|
return this._instance.options.selectable;
|
|
},
|
|
// override set `option`
|
|
option: function(option, value) {
|
|
if (this.wasInit() && !this.isLocked()) {
|
|
if ((option == 'selectable') && (value != this.extSelectable())) {
|
|
if (value) {
|
|
this._selectableInit();
|
|
} else {
|
|
this._selectableDone();
|
|
}
|
|
}
|
|
if ((option == 'multiSelectable') && (value != this._instance.options.multiSelectable)) {
|
|
this._multiSelectable(value);
|
|
}
|
|
if ((option == 'fullRow') && (value != this._instance.options.fullRow)) {
|
|
this._fullRow(value);
|
|
}
|
|
if ((option == 'textSelection') && (value != this._instance.options.textSelection)) {
|
|
this._selectable(value);
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(option, value);
|
|
},
|
|
// done selectable
|
|
_selectableDone: function(destroy) {
|
|
if (this._instance.jQuery.attr('tabindex') == 0) {
|
|
this._instance.jQuery.removeAttr('tabindex');
|
|
}
|
|
if (!this._instance.options.textSelection) {
|
|
this._selectable(true);
|
|
}
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
this._instance.jQuery.off(this._private.nameSpace, '.aciTreeLine,.aciTreeItem').off(this._private.nameSpace, '.aciTreeItem');
|
|
domApi.removeClass(this._instance.jQuery[0], ['aciTreeFocus', 'aciTreeFullRow']);
|
|
this._instance.jQuery.removeAttr('aria-multiselectable');
|
|
this._instance.focus = false;
|
|
this._private.spinPoint = null;
|
|
if (!destroy) {
|
|
// remove selection
|
|
this._unselect(this.selected());
|
|
var focused = this.focused();
|
|
if (focused.length) {
|
|
this.blur(focused);
|
|
}
|
|
}
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._selectableDone(true);
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
|
|
};
|
|
|
|
// extend the base aciTree class and add the selectable stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_selectable, 'aciTreeSelectable');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds checkbox support to aciTree,
|
|
* should be used with the selectable extension.
|
|
*
|
|
* The are a few extra properties for the item data:
|
|
*
|
|
* {
|
|
* ...
|
|
* checkbox: true, // TRUE (default) means the item will have a checkbox (can be omitted if the `radio` extension is not used)
|
|
* checked: false, // if should be checked or not
|
|
* ...
|
|
* }
|
|
*
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
checkbox: false, // if TRUE then each item will have a checkbox
|
|
checkboxChain: true,
|
|
// if TRUE the selection will propagate to the parents/children
|
|
// if -1 the selection will propagate only to parents
|
|
// if +1 the selection will propagate only to children
|
|
// if FALSE the selection will not propagate in any way
|
|
checkboxBreak: true, // if TRUE then a missing checkbox will break the chaining
|
|
checkboxClick: false // if TRUE then a click will trigger a state change only when made over the checkbox itself
|
|
};
|
|
|
|
// aciTree checkbox extension
|
|
|
|
var aciTree_checkbox = {
|
|
// init checkbox
|
|
_checkboxInit: function() {
|
|
this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) {
|
|
switch (eventName) {
|
|
case 'loaded':
|
|
// check/update on item load
|
|
api._checkboxLoad(item);
|
|
break;
|
|
}
|
|
}).bind('keydown' + this._private.nameSpace, this.proxy(function(e) {
|
|
switch (e.which) {
|
|
case 32: // space
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable() && !e.ctrlKey) {
|
|
var item = this.focused();
|
|
if (this.hasCheckbox(item) && this.isEnabled(item)) {
|
|
if (this.isChecked(item)) {
|
|
this.uncheck(item);
|
|
} else {
|
|
this.check(item);
|
|
}
|
|
e.stopImmediatePropagation();
|
|
// prevent page scroll
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
})).on('click' + this._private.nameSpace, '.aciTreeItem', this.proxy(function(e) {
|
|
if (!this._instance.options.checkboxClick || $(e.target).is('.aciTreeCheck')) {
|
|
var item = this.itemFrom(e.target);
|
|
if (this.hasCheckbox(item) && this.isEnabled(item) && (!this.extSelectable || !this.extSelectable() || (!e.ctrlKey && !e.shiftKey))) {
|
|
// change state on click
|
|
if (this.isChecked(item)) {
|
|
this.uncheck(item);
|
|
} else {
|
|
this.check(item);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}));
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extCheckbox()) {
|
|
this._checkboxInit();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// override `_itemHook`
|
|
_itemHook: function(parent, item, itemData, level) {
|
|
if (this.extCheckbox()) {
|
|
// support `radio` extension
|
|
var radio = this.extRadio && this.hasRadio(item);
|
|
if (!radio && (itemData.checkbox || ((itemData.checkbox === undefined) && (!this.extRadio || !this.extRadio())))) {
|
|
this._checkboxDOM.add(item, itemData);
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(parent, item, itemData, level);
|
|
},
|
|
// low level DOM functions
|
|
_checkboxDOM: {
|
|
// add item checkbox
|
|
add: function(item, itemData) {
|
|
domApi.addClass(item[0], itemData.checked ? ['aciTreeCheckbox', 'aciTreeChecked'] : 'aciTreeCheckbox');
|
|
var text = domApi.childrenByClass(item[0].firstChild, 'aciTreeText');
|
|
var parent = text.parentNode;
|
|
var label = window.document.createElement('LABEL');
|
|
var check = window.document.createElement('SPAN');
|
|
check.className = 'aciTreeCheck';
|
|
label.appendChild(check);
|
|
label.appendChild(text);
|
|
parent.appendChild(label);
|
|
item[0].firstChild.setAttribute('aria-checked', !!itemData.checked);
|
|
},
|
|
// remove item checkbox
|
|
remove: function(item) {
|
|
domApi.removeClass(item[0], ['aciTreeCheckbox', 'aciTreeChecked', 'aciTreeTristate']);
|
|
var text = domApi.childrenByClass(item[0].firstChild, 'aciTreeText');
|
|
var label = text.parentNode;
|
|
var parent = label.parentNode;
|
|
parent.replaceChild(text, label)
|
|
item[0].firstChild.removeAttribute('aria-checked');
|
|
},
|
|
// (un)check items
|
|
check: function(items, state) {
|
|
domApi.toggleListClass(items.toArray(), 'aciTreeChecked', state, function(node) {
|
|
node.firstChild.setAttribute('aria-checked', state);
|
|
});
|
|
},
|
|
// (un)set tristate items
|
|
tristate: function(items, state) {
|
|
domApi.toggleListClass(items.toArray(), 'aciTreeTristate', state);
|
|
}
|
|
},
|
|
// update items on load, starting from the loaded node
|
|
_checkboxLoad: function(item) {
|
|
if (this._instance.options.checkboxChain === false) {
|
|
// do not update on load
|
|
return;
|
|
}
|
|
var state = undefined;
|
|
if (this.hasCheckbox(item)) {
|
|
if (this.isChecked(item)) {
|
|
if (!this.checkboxes(this.children(item, false, true), true).length) {
|
|
// the item is checked but no children are, check them all
|
|
state = true;
|
|
}
|
|
} else {
|
|
// the item is not checked, uncheck all children
|
|
state = false;
|
|
}
|
|
}
|
|
this._checkboxUpdate(item, state);
|
|
},
|
|
// get children list
|
|
_checkboxChildren: function(item) {
|
|
if (this._instance.options.checkboxBreak) {
|
|
var list = [];
|
|
var process = this.proxy(function(item) {
|
|
var children = this.children(item, false, true);
|
|
children.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
// break on missing checkbox
|
|
if (this.hasCheckbox(item)) {
|
|
list.push(element);
|
|
process(item);
|
|
}
|
|
}, true));
|
|
});
|
|
process(item);
|
|
return $(list);
|
|
} else {
|
|
var children = this.children(item, true, true);
|
|
return this.checkboxes(children);
|
|
}
|
|
},
|
|
// update checkbox state
|
|
_checkboxUpdate: function(item, state) {
|
|
// update children
|
|
var checkDown = this.proxy(function(item, count, state) {
|
|
var children = this.children(item, false, true);
|
|
var total = 0;
|
|
var checked = 0;
|
|
children.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
var subCount = {
|
|
total: 0,
|
|
checked: 0
|
|
};
|
|
if (this.hasCheckbox(item)) {
|
|
if ((state !== undefined) && (this._instance.options.checkboxChain !== -1)) {
|
|
this._checkboxDOM.check(item, state);
|
|
}
|
|
total++;
|
|
if (this.isChecked(item)) {
|
|
checked++;
|
|
}
|
|
checkDown(item, subCount, state);
|
|
} else {
|
|
if (this._instance.options.checkboxBreak) {
|
|
var reCount = {
|
|
total: 0,
|
|
checked: 0
|
|
};
|
|
checkDown(item, reCount);
|
|
} else {
|
|
checkDown(item, subCount, state);
|
|
}
|
|
}
|
|
total += subCount.total;
|
|
checked += subCount.checked;
|
|
}, true));
|
|
if (item) {
|
|
this._checkboxDOM.tristate(item, (checked > 0) && (checked != total));
|
|
count.total += total;
|
|
count.checked += checked;
|
|
}
|
|
});
|
|
var count = {
|
|
total: 0,
|
|
checked: 0
|
|
};
|
|
checkDown(item, count, state);
|
|
// update parents
|
|
var checkUp = this.proxy(function(item, tristate, state) {
|
|
var parent = this.parent(item);
|
|
if (parent.length) {
|
|
if (!tristate) {
|
|
var children = this._checkboxChildren(parent);
|
|
var checked = this.checkboxes(children, true).length;
|
|
var tristate = (checked > 0) && (checked != children.length);
|
|
}
|
|
if (this.hasCheckbox(parent)) {
|
|
if ((state !== undefined) && (this._instance.options.checkboxChain !== 1)) {
|
|
this._checkboxDOM.check(parent, tristate ? true : state);
|
|
}
|
|
this._checkboxDOM.tristate(parent, tristate);
|
|
checkUp(parent, tristate, state);
|
|
} else {
|
|
if (this._instance.options.checkboxBreak) {
|
|
checkUp(parent);
|
|
} else {
|
|
checkUp(parent, tristate, state);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
checkUp(item, undefined, state);
|
|
},
|
|
// test if item have a checkbox
|
|
hasCheckbox: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeCheckbox');
|
|
},
|
|
// add checkbox
|
|
addCheckbox: function(item, options) {
|
|
options = this._options(options, 'checkboxadded', 'addcheckboxfail', 'wascheckbox', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeaddcheckbox', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.hasCheckbox(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
var process = function() {
|
|
this._checkboxDOM.add(item, {
|
|
});
|
|
this._success(item, options);
|
|
};
|
|
// support `radio` extension
|
|
if (this.extRadio && this.hasRadio(item)) {
|
|
// remove radio first
|
|
this.removeRadio(item, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
} else {
|
|
process.apply(this);
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// remove checkbox
|
|
removeCheckbox: function(item, options) {
|
|
options = this._options(options, 'checkboxremoved', 'removecheckboxfail', 'notcheckbox', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeremovecheckbox', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.hasCheckbox(item)) {
|
|
this._checkboxDOM.remove(item);
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if it's checked
|
|
isChecked: function(item) {
|
|
if (this.hasCheckbox(item)) {
|
|
return domApi.hasClass(item[0], 'aciTreeChecked');
|
|
}
|
|
// support `radio` extension
|
|
if (this._super) {
|
|
// call the parent
|
|
return this._super(item);
|
|
}
|
|
return false;
|
|
},
|
|
// check checkbox
|
|
check: function(item, options) {
|
|
if (this.extCheckbox && this.hasCheckbox(item)) {
|
|
options = this._options(options, 'checked', 'checkfail', 'waschecked', item);
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforecheck', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isChecked(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
this._checkboxDOM.check(item, true);
|
|
if (this._instance.options.checkboxChain !== false) {
|
|
// chain them
|
|
this._checkboxUpdate(item, true);
|
|
}
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
// support `radio` extension
|
|
if (this._super) {
|
|
// call the parent
|
|
this._super(item, options);
|
|
} else {
|
|
this._trigger(item, 'checkfail', options);
|
|
this._fail(item, options);
|
|
}
|
|
}
|
|
},
|
|
// uncheck checkbox
|
|
uncheck: function(item, options) {
|
|
if (this.extCheckbox && this.hasCheckbox(item)) {
|
|
options = this._options(options, 'unchecked', 'uncheckfail', 'notchecked', item);
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeuncheck', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isChecked(item)) {
|
|
this._checkboxDOM.check(item, false);
|
|
if (this._instance.options.checkboxChain !== false) {
|
|
// chain them
|
|
this._checkboxUpdate(item, false);
|
|
}
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
// support `radio` extension
|
|
if (this._super) {
|
|
// call the parent
|
|
this._super(item, options);
|
|
} else {
|
|
this._trigger(item, 'uncheckfail', options);
|
|
this._fail(item, options);
|
|
}
|
|
}
|
|
},
|
|
// filter items with checkbox by state (if set)
|
|
checkboxes: function(items, state) {
|
|
if (state !== undefined) {
|
|
return $(domApi.withClass(items.toArray(), state ? ['aciTreeCheckbox', 'aciTreeChecked'] : 'aciTreeCheckbox', state ? null : 'aciTreeChecked'));
|
|
}
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeCheckbox'));
|
|
},
|
|
// override `_serialize`
|
|
_serialize: function(item, callback) {
|
|
var data = this._super(item, callback);
|
|
if (data && this.extCheckbox()) {
|
|
if (data.hasOwnProperty('checkbox')) {
|
|
data.checkbox = this.hasCheckbox(item);
|
|
data.checked = this.isChecked(item);
|
|
} else if (this.hasCheckbox(item)) {
|
|
if (this.extRadio && this.extRadio()) {
|
|
data.checkbox = true;
|
|
}
|
|
data.checked = this.isChecked(item);
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
// override `serialize`
|
|
serialize: function(item, what, callback) {
|
|
if (what == 'checkbox') {
|
|
var serialized = '';
|
|
var children = this.children(item, true, true);
|
|
this.checkboxes(children, true).each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (callback) {
|
|
serialized += callback.call(this, item, what, this.getId(item));
|
|
} else {
|
|
serialized += this._instance.options.serialize.call(this, item, what, this.getId(item));
|
|
}
|
|
}, true));
|
|
return serialized;
|
|
}
|
|
return this._super(item, what, callback);
|
|
},
|
|
// test if item is in tristate
|
|
isTristate: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeTristate');
|
|
},
|
|
// filter tristate items
|
|
tristate: function(items) {
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeTristate'));
|
|
},
|
|
// test if checkbox is enabled
|
|
extCheckbox: function() {
|
|
return this._instance.options.checkbox;
|
|
},
|
|
// override set `option`
|
|
option: function(option, value) {
|
|
if (this.wasInit() && !this.isLocked()) {
|
|
if ((option == 'checkbox') && (value != this.extCheckbox())) {
|
|
if (value) {
|
|
this._checkboxInit();
|
|
} else {
|
|
this._checkboxDone();
|
|
}
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(option, value);
|
|
},
|
|
// done checkbox
|
|
_checkboxDone: function(destroy) {
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
this._instance.jQuery.off(this._private.nameSpace, '.aciTreeItem');
|
|
if (!destroy) {
|
|
// remove checkboxes
|
|
this.checkboxes(this.children(null, true, true)).each(this.proxy(function(element) {
|
|
this.removeCheckbox($(element));
|
|
}, true));
|
|
}
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._checkboxDone(true);
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
|
|
};
|
|
|
|
// extend the base aciTree class and add the checkbox stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_checkbox, 'aciTreeCheckbox');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds radio-button support to aciTree,
|
|
* should be used with the selectable extension.
|
|
*
|
|
* The are a few extra properties for the item data:
|
|
*
|
|
* {
|
|
* ...
|
|
* radio: true, // TRUE (default) means the item will have a radio button (can be omitted if the `checkbox` extension is not used)
|
|
* checked: false, // if should be checked or not
|
|
* ...
|
|
* }
|
|
*
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
radio: false, // if TRUE then each item will have a radio button
|
|
radioChain: true, // if TRUE the selection will propagate to the parents/children
|
|
radioBreak: true, // if TRUE then a missing radio button will break the chaining
|
|
radioClick: false // if TRUE then a click will trigger a state change only when made over the radio-button itself
|
|
};
|
|
|
|
// aciTree radio extension
|
|
|
|
var aciTree_radio = {
|
|
// init radio
|
|
_radioInit: function() {
|
|
this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) {
|
|
switch (eventName) {
|
|
case 'loaded':
|
|
if (item) {
|
|
// check/update on item load
|
|
api._radioLoad(item);
|
|
}
|
|
break;
|
|
}
|
|
}).bind('keydown' + this._private.nameSpace, this.proxy(function(e) {
|
|
switch (e.which) {
|
|
case 32: // space
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable() && !e.ctrlKey) {
|
|
var item = this.focused();
|
|
if (this.hasRadio(item) && this.isEnabled(item)) {
|
|
if (!this.isChecked(item)) {
|
|
this.check(item);
|
|
}
|
|
e.stopImmediatePropagation();
|
|
// prevent page scroll
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
})).on('click' + this._private.nameSpace, '.aciTreeItem', this.proxy(function(e) {
|
|
if (!this._instance.options.radioClick || $(e.target).is('.aciTreeCheck')) {
|
|
var item = this.itemFrom(e.target);
|
|
if (this.hasRadio(item) && this.isEnabled(item) && (!this.extSelectable || !this.extSelectable() || (!e.ctrlKey && !e.shiftKey))) {
|
|
// change state on click
|
|
if (!this.isChecked(item)) {
|
|
this.check(item);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}));
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extRadio()) {
|
|
this._radioInit();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// override `_itemHook`
|
|
_itemHook: function(parent, item, itemData, level) {
|
|
if (this.extRadio()) {
|
|
// support `checkbox` extension
|
|
var checkbox = this.extCheckbox && this.hasCheckbox(item);
|
|
if (!checkbox && (itemData.radio || ((itemData.radio === undefined) && (!this.extCheckbox || !this.extCheckbox())))) {
|
|
this._radioDOM.add(item, itemData);
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(parent, item, itemData, level);
|
|
},
|
|
// low level DOM functions
|
|
_radioDOM: {
|
|
// add item radio
|
|
add: function(item, itemData) {
|
|
domApi.addClass(item[0], itemData.checked ? ['aciTreeRadio', 'aciTreeChecked'] : 'aciTreeRadio');
|
|
var text = domApi.childrenByClass(item[0].firstChild, 'aciTreeText');
|
|
var parent = text.parentNode;
|
|
var label = window.document.createElement('LABEL');
|
|
var check = window.document.createElement('SPAN');
|
|
check.className = 'aciTreeCheck';
|
|
label.appendChild(check);
|
|
label.appendChild(text);
|
|
parent.appendChild(label);
|
|
item[0].firstChild.setAttribute('aria-checked', !!itemData.checked);
|
|
},
|
|
// remove item radio
|
|
remove: function(item) {
|
|
domApi.removeClass(item[0], ['aciTreeRadio', 'aciTreeChecked']);
|
|
var text = domApi.childrenByClass(item[0].firstChild, 'aciTreeText');
|
|
var label = text.parentNode;
|
|
var parent = label.parentNode;
|
|
parent.replaceChild(text, label)
|
|
item[0].firstChild.removeAttribute('aria-checked');
|
|
},
|
|
// (un)check items
|
|
check: function(items, state) {
|
|
domApi.toggleListClass(items.toArray(), 'aciTreeChecked', state, function(node) {
|
|
node.firstChild.setAttribute('aria-checked', state);
|
|
});
|
|
}
|
|
},
|
|
// update item on load
|
|
_radioLoad: function(item) {
|
|
if (!this._instance.options.radioChain) {
|
|
// do not update on load
|
|
return;
|
|
}
|
|
if (this.hasRadio(item)) {
|
|
if (this.isChecked(item)) {
|
|
if (!this.radios(this.children(item, false, true), true).length) {
|
|
// the item is checked but no children are, check the children
|
|
this._radioUpdate(item, true);
|
|
}
|
|
} else {
|
|
// the item is not checked, uncheck children
|
|
this._radioUpdate(item);
|
|
}
|
|
}
|
|
},
|
|
// get children list
|
|
_radioChildren: function(item) {
|
|
if (this._instance.options.radioBreak) {
|
|
var list = [];
|
|
var process = this.proxy(function(item) {
|
|
var children = this.children(item, false, true);
|
|
children.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
// break on missing radio
|
|
if (this.hasRadio(item)) {
|
|
list.push(element);
|
|
process(item);
|
|
}
|
|
}, true));
|
|
});
|
|
process(item);
|
|
return $(list);
|
|
} else {
|
|
var children = this.children(item, true, true);
|
|
return this.radios(children);
|
|
}
|
|
},
|
|
// get children across items
|
|
_radioLevel: function(items) {
|
|
var list = [];
|
|
items.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
var children = this.children(item, false, true);
|
|
children.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (!this._instance.options.radioBreak || this.hasRadio(item)) {
|
|
list.push(element);
|
|
}
|
|
}, true));
|
|
}, true));
|
|
return $(list);
|
|
},
|
|
// update radio state
|
|
_radioUpdate: function(item, state) {
|
|
// update siblings
|
|
var siblings = this.proxy(function(item) {
|
|
var siblings = this.siblings(item, true);
|
|
this._radioDOM.check(this.radios(siblings), false);
|
|
siblings.each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (!this._instance.options.radioBreak || this.hasRadio(item)) {
|
|
this._radioDOM.check(this._radioChildren(item), false);
|
|
}
|
|
}, true));
|
|
});
|
|
if (state) {
|
|
siblings(item);
|
|
}
|
|
// update children
|
|
var checkDown = this.proxy(function(item) {
|
|
var children = this._radioLevel(item);
|
|
var radios = this.radios(children);
|
|
if (radios.length) {
|
|
var checked = this.radios(children, true);
|
|
if (checked.length) {
|
|
checked = checked.first();
|
|
this._radioDOM.check(checked, true);
|
|
siblings(checked);
|
|
checkDown(checked);
|
|
} else {
|
|
checked = radios.first();
|
|
this._radioDOM.check(checked, true);
|
|
siblings(checked);
|
|
checkDown(checked);
|
|
}
|
|
} else if (children.length) {
|
|
checkDown(children);
|
|
}
|
|
});
|
|
if (state) {
|
|
checkDown(item);
|
|
} else {
|
|
this._radioDOM.check(this._radioChildren(item), false);
|
|
}
|
|
// update parents
|
|
var checkUp = this.proxy(function(item) {
|
|
var parent = this.parent(item);
|
|
if (parent.length) {
|
|
if (this.hasRadio(parent)) {
|
|
if (state) {
|
|
siblings(parent);
|
|
}
|
|
this._radioDOM.check(parent, state);
|
|
checkUp(parent);
|
|
} else {
|
|
if (!this._instance.options.radioBreak) {
|
|
if (state) {
|
|
siblings(parent);
|
|
}
|
|
checkUp(parent);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (state !== undefined) {
|
|
checkUp(item);
|
|
}
|
|
},
|
|
// test if item have a radio
|
|
hasRadio: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeRadio');
|
|
},
|
|
// add radio button
|
|
addRadio: function(item, options) {
|
|
options = this._options(options, 'radioadded', 'addradiofail', 'wasradio', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeaddradio', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.hasRadio(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
var process = function() {
|
|
this._radioDOM.add(item, {
|
|
});
|
|
this._success(item, options);
|
|
};
|
|
// support `checkbox` extension
|
|
if (this.extCheckbox && this.hasCheckbox(item)) {
|
|
// remove checkbox first
|
|
this.removeCheckbox(item, this._inner(options, {
|
|
success: process,
|
|
fail: options.fail
|
|
}));
|
|
} else {
|
|
process.apply(this);
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// remove radio button
|
|
removeRadio: function(item, options) {
|
|
options = this._options(options, 'radioremoved', 'removeradiofail', 'notradio', item);
|
|
if (this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeremoveradio', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.hasRadio(item)) {
|
|
this._radioDOM.remove(item);
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if it's checked
|
|
isChecked: function(item) {
|
|
if (this.hasRadio(item)) {
|
|
return domApi.hasClass(item[0], 'aciTreeChecked');
|
|
}
|
|
// support `checkbox` extension
|
|
if (this._super) {
|
|
// call the parent
|
|
return this._super(item);
|
|
}
|
|
return false;
|
|
},
|
|
// check radio button
|
|
check: function(item, options) {
|
|
if (this.extRadio && this.hasRadio(item)) {
|
|
options = this._options(options, 'checked', 'checkfail', 'waschecked', item);
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforecheck', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isChecked(item)) {
|
|
this._notify(item, options);
|
|
} else {
|
|
this._radioDOM.check(item, true);
|
|
if (this._instance.options.radioChain) {
|
|
// chain them
|
|
this._radioUpdate(item, true);
|
|
}
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
// support `checkbox` extension
|
|
if (this._super) {
|
|
// call the parent
|
|
this._super(item, options);
|
|
} else {
|
|
this._trigger(item, 'checkfail', options);
|
|
this._fail(item, options);
|
|
}
|
|
}
|
|
},
|
|
// uncheck radio button
|
|
uncheck: function(item, options) {
|
|
if (this.extRadio && this.hasRadio(item)) {
|
|
options = this._options(options, 'unchecked', 'uncheckfail', 'notchecked', item);
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeuncheck', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
if (this.isChecked(item)) {
|
|
this._radioDOM.check(item, false);
|
|
if (this._instance.options.radioChain) {
|
|
// chain them
|
|
this._radioUpdate(item, false);
|
|
}
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
// support `checkbox` extension
|
|
if (this._super) {
|
|
// call the parent
|
|
this._super(item, options);
|
|
} else {
|
|
this._trigger(item, 'uncheckfail', options);
|
|
this._fail(item, options);
|
|
}
|
|
}
|
|
},
|
|
// filter items with radio by state (if set)
|
|
radios: function(items, state) {
|
|
if (state !== undefined) {
|
|
return $(domApi.withClass(items.toArray(), state ? ['aciTreeRadio', 'aciTreeChecked'] : 'aciTreeRadio', state ? null : 'aciTreeChecked'));
|
|
}
|
|
return $(domApi.withClass(items.toArray(), 'aciTreeRadio'));
|
|
},
|
|
// override `_serialize`
|
|
_serialize: function(item, callback) {
|
|
var data = this._super(item, callback);
|
|
if (data && this.extRadio()) {
|
|
if (data.hasOwnProperty('radio')) {
|
|
data.radio = this.hasRadio(item);
|
|
data.checked = this.isChecked(item);
|
|
} else if (this.hasRadio(item)) {
|
|
if (this.extCheckbox && this.extCheckbox()) {
|
|
data.radio = true;
|
|
}
|
|
data.checked = this.isChecked(item);
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
// override `serialize`
|
|
serialize: function(item, what, callback) {
|
|
if (what == 'radio') {
|
|
var serialized = '';
|
|
var children = this.children(item, true, true);
|
|
this.radios(children, true).each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (callback) {
|
|
serialized += callback.call(this, item, what, this.getId(item));
|
|
} else {
|
|
serialized += this._instance.options.serialize.call(this, item, what, this.getId(item));
|
|
}
|
|
}, true));
|
|
return serialized;
|
|
}
|
|
return this._super(item, what, callback);
|
|
},
|
|
// test if radio is enabled
|
|
extRadio: function() {
|
|
return this._instance.options.radio;
|
|
},
|
|
// override set `option`
|
|
option: function(option, value) {
|
|
if (this.wasInit() && !this.isLocked()) {
|
|
if ((option == 'radio') && (value != this.extRadio())) {
|
|
if (value) {
|
|
this._radioInit();
|
|
} else {
|
|
this._radioDone();
|
|
}
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(option, value);
|
|
},
|
|
// done radio
|
|
_radioDone: function(destroy) {
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
this._instance.jQuery.off(this._private.nameSpace, '.aciTreeItem');
|
|
if (!destroy) {
|
|
// remove radios
|
|
this.radios(this.children(null, true, true)).each(this.proxy(function(element) {
|
|
this.removeRadio($(element));
|
|
}, true));
|
|
}
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._radioDone(true);
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
|
|
};
|
|
|
|
// extend the base aciTree class and add the radio stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_radio, 'aciTreeRadio');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds multiple column support to aciTree.
|
|
*
|
|
* The `columnData` option is used to tell what are the columns and show one or
|
|
* more values that will be read from the item data object.
|
|
*
|
|
* Column data is an array of column definitions, each column definition is
|
|
* an object:
|
|
*
|
|
* {
|
|
* width: 100,
|
|
* props: 'column_x',
|
|
* value: 'default'
|
|
* }
|
|
*
|
|
* where the `width` is the column width in [px], if undefined - then the value
|
|
* from the CSS will be used; the `props` is the property name that will be
|
|
* read from the item data, if undefined (or the `item-data[column.props]`
|
|
* is undefined) then a default value will be set for the column: the `value`.
|
|
*
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
columnData: [] // column definitions list
|
|
};
|
|
|
|
// aciTree columns extension
|
|
// adds item columns, set width with CSS or using the API
|
|
|
|
var aciTree_column = {
|
|
__extend: function() {
|
|
// add extra data
|
|
$.extend(this._private, {
|
|
propsIndex: { // column index cache
|
|
}
|
|
});
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this._instance.options.columnData.length) {
|
|
// check column width
|
|
var found = false, data;
|
|
for (var i in this._instance.options.columnData) {
|
|
data = this._instance.options.columnData[i];
|
|
if (data.width !== undefined) {
|
|
// update column width
|
|
this._updateCss('.aciTree.aciTree' + this._instance.index + ' .aciTreeColumn' + i, 'width:' + data.width + 'px;');
|
|
found = true;
|
|
}
|
|
this._private.propsIndex[data.props] = i;
|
|
}
|
|
if (found) {
|
|
// at least a column width set
|
|
this._updateWidth();
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// read property value from a CSS class name
|
|
_getCss: function(className, property, numeric) {
|
|
var id = '_getCss_' + window.String(className).replace(/[^a-z0-9_-]/ig, '_');
|
|
var test = $('body').find('#' + id);
|
|
if (!test.length) {
|
|
if (className instanceof Array) {
|
|
var style = '', end = '';
|
|
for (var i in className) {
|
|
style += '<div class="' + className[i] + '">';
|
|
end += '</div>';
|
|
}
|
|
style += end;
|
|
} else {
|
|
var style = '<div class="' + className + '"></div>';
|
|
}
|
|
$('body').append('<div id="' + id + '" style="position:relative;display:inline-block;width:0px;height:0px;line-height:0px;overflow:hidden">' + style + '</div>');
|
|
test = $('body').find('#' + id);
|
|
}
|
|
var value = test.find('*:last').css(property);
|
|
if (numeric) {
|
|
value = parseInt(value);
|
|
if (isNaN(value)) {
|
|
value = null;
|
|
}
|
|
}
|
|
return value;
|
|
},
|
|
// dynamically change a CSS class definition
|
|
_updateCss: function(className, definition) {
|
|
var id = '_updateCss_' + window.String(className).replace('>', '_gt_').replace(/[^a-z0-9_-]/ig, '_');
|
|
var style = '<style id="' + id + '" type="text/css">' + className + '{' + definition + '}</style>';
|
|
var test = $('body').find('#' + id);
|
|
if (test.length) {
|
|
test.replaceWith(style);
|
|
} else {
|
|
$('body').prepend(style);
|
|
}
|
|
},
|
|
// get column width
|
|
// `index` is the #0 based column index
|
|
getWidth: function(index) {
|
|
if ((index >= 0) && (index < this.columns())) {
|
|
return this._getCss(['aciTree aciTree' + this._instance.index, 'aciTreeColumn' + index], 'width', true);
|
|
}
|
|
return null;
|
|
},
|
|
// set column width
|
|
// `index` is the #0 based column index
|
|
setWidth: function(index, width) {
|
|
if ((index >= 0) && (index < this.columns())) {
|
|
this._updateCss('.aciTree.aciTree' + this._instance.index + ' .aciTreeColumn' + index, 'width:' + width + 'px;');
|
|
this._updateWidth();
|
|
}
|
|
},
|
|
// update item margins
|
|
_updateWidth: function() {
|
|
var width = 0;
|
|
for (var i in this._instance.options.columnData) {
|
|
if (this.isColumn(i)) {
|
|
width += this.getWidth(i);
|
|
}
|
|
}
|
|
var icon = this._getCss(['aciTree', 'aciTreeIcon'], 'width', true);
|
|
// add item padding
|
|
width += this._getCss(['aciTree', 'aciTreeItem'], 'padding-left', true) + this._getCss(['aciTree', 'aciTreeItem'], 'padding-right', true);
|
|
this._updateCss('.aciTree.aciTree' + this._instance.index + ' .aciTreeItem', 'margin-right:' + (icon + width) + 'px;');
|
|
this._updateCss('.aciTree[dir=rtl].aciTree' + this._instance.index + ' .aciTreeItem', 'margin-right:0;margin-left:' + (icon + width) + 'px;');
|
|
},
|
|
// test if column is visible
|
|
// `index` is the #0 based column index
|
|
isColumn: function(index) {
|
|
if ((index >= 0) && (index < this.columns())) {
|
|
return this._getCss(['aciTree aciTree' + this._instance.index, 'aciTreeColumn' + index], 'display') != 'none';
|
|
}
|
|
return false;
|
|
},
|
|
// get column index by `props`
|
|
// return -1 if the column does not exists
|
|
columnIndex: function(props) {
|
|
if (this._private.propsIndex[props] !== undefined) {
|
|
return this._private.propsIndex[props];
|
|
}
|
|
return -1;
|
|
},
|
|
// get the column count
|
|
columns: function() {
|
|
return this._instance.options.columnData.length;
|
|
},
|
|
// set column to be visible or hidden
|
|
// `index` is the #0 based column index
|
|
// if `show` is undefined then the column visibility will be toggled
|
|
toggleColumn: function(index, show) {
|
|
if ((index >= 0) && (index < this.columns())) {
|
|
if (show === undefined) {
|
|
var show = !this.isColumn(index);
|
|
}
|
|
this._updateCss('.aciTree.aciTree' + this._instance.index + ' .aciTreeColumn' + index, 'display:' + (show ? 'inherit' : 'none') + ';');
|
|
this._updateWidth();
|
|
}
|
|
},
|
|
// override `_itemHook`
|
|
_itemHook: function(parent, item, itemData, level) {
|
|
if (this.columns()) {
|
|
var position = domApi.childrenByClass(item[0].firstChild, 'aciTreeEntry'), data, column;
|
|
for (var i in this._instance.options.columnData) {
|
|
data = this._instance.options.columnData[i];
|
|
column = this._createColumn(itemData, data, i);
|
|
position.insertBefore(column, position.firstChild);
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(parent, item, itemData, level);
|
|
},
|
|
// create column markup
|
|
// `itemData` item data object
|
|
// `columnData` column data definition
|
|
// `index` is the #0 based column index
|
|
_createColumn: function(itemData, columnData, index) {
|
|
var value = columnData.props && (itemData[columnData.props] !== undefined) ? itemData[columnData.props] :
|
|
((columnData.value === undefined) ? '' : columnData.value);
|
|
var column = window.document.createElement('DIV');
|
|
column.className = 'aciTreeColumn aciTreeColumn' + index;
|
|
column.innerHTML = value.length ? value : ' ';
|
|
return column;
|
|
},
|
|
// set column content
|
|
// `options.index` the #0 based column index
|
|
// `options.value` is the new content
|
|
// `options.oldValue` will keep the old content
|
|
setColumn: function(item, options) {
|
|
options = this._options(options, 'columnset', 'columnfail', 'wascolumn', item);
|
|
if (this.isItem(item) && (options.index >= 0) && (options.index < this.columns())) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforecolumn', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var data = this.itemData(item);
|
|
// keep the old one
|
|
options.oldValue = data[this._instance.options.columnData[options.index].props];
|
|
if (options.value == options.oldValue) {
|
|
this._notify(item, options);
|
|
} else {
|
|
// set the column
|
|
item.children('.aciTreeLine').find('.aciTreeColumn' + options.index).html(options.value);
|
|
// remember this one
|
|
data[this._instance.options.columnData[options.index].props] = options.value;
|
|
this._success(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// get column content
|
|
getColumn: function(item, index) {
|
|
if ((index >= 0) && (index < this.columns())) {
|
|
var data = this.itemData(item);
|
|
return data ? data[this._instance.options.columnData[index].props] : null;
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// extend the base aciTree class and add the columns stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_column, 'aciTreeColumn');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds inplace edit support to aciTree,
|
|
* should be used with the selectable extension.
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
editable: false, // if TRUE then each item will be inplace editable
|
|
editDelay: 250 // how many [ms] to wait (with mouse down) before starting the edit (on mouse release)
|
|
};
|
|
|
|
// aciTree editable extension
|
|
// add inplace item editing by pressing F2 key or mouse click (to enter edit mode)
|
|
// press enter/escape to save/cancel the text edit
|
|
|
|
var aciTree_editable = {
|
|
__extend: function() {
|
|
// add extra data
|
|
$.extend(this._private, {
|
|
editTimestamp: null
|
|
});
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// init editable
|
|
_editableInit: function() {
|
|
this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) {
|
|
switch (eventName) {
|
|
case 'blurred':
|
|
// support `selectable` extension
|
|
var item = api.edited();
|
|
if (item.length) {
|
|
// cancel edit/save the changes
|
|
api.endEdit();
|
|
}
|
|
break;
|
|
case 'deselected':
|
|
// support `selectable` extension
|
|
if (api.isEdited(item)) {
|
|
// cancel edit/save the changes
|
|
api.endEdit();
|
|
}
|
|
break;
|
|
}
|
|
}).bind('click' + this._private.nameSpace, this.proxy(function() {
|
|
// click on the tree
|
|
var item = this.edited();
|
|
if (item.length) {
|
|
// cancel edit/save the changes
|
|
this.endEdit();
|
|
}
|
|
})).bind('keydown' + this._private.nameSpace, this.proxy(function(e) {
|
|
switch (e.which) {
|
|
case 113: // F2
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable()) {
|
|
var item = this.focused();
|
|
if (item.length && !this.isEdited(item) && this.isEnabled(item)) {
|
|
// enable edit on F2 key
|
|
this.edit(item);
|
|
// prevent default F2 key function
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
})).on('mousedown' + this._private.nameSpace, '.aciTreeItem', this.proxy(function(e) {
|
|
if ($(e.target).is('.aciTreeItem,.aciTreeText')) {
|
|
this._private.editTimestamp = $.now();
|
|
}
|
|
})).on('mouseup' + this._private.nameSpace, '.aciTreeItem', this.proxy(function(e) {
|
|
if ($(e.target).is('.aciTreeItem,.aciTreeText')) {
|
|
var passed = $.now() - this._private.editTimestamp;
|
|
// start edit only after N [ms] but before N * 4 [ms] have passed
|
|
if ((passed > this._instance.options.editDelay) && (passed < this._instance.options.editDelay * 4)) {
|
|
var item = this.itemFrom(e.target);
|
|
if ((!this.extSelectable || !this.extSelectable() || (this.isFocused(item) && (this.selected().length == 1))) && this.isEnabled(item)) {
|
|
// edit on mouseup
|
|
this.edit(item);
|
|
}
|
|
}
|
|
}
|
|
})).on('keydown' + this._private.nameSpace, 'input[type=text]', this.proxy(function(e) {
|
|
// key handling
|
|
switch (e.which) {
|
|
case 13: // enter
|
|
this.itemFrom(e.target).focus();
|
|
this.endEdit();
|
|
e.stopPropagation();
|
|
break;
|
|
case 27: // escape
|
|
this.itemFrom(e.target).focus();
|
|
this.endEdit({
|
|
save: false
|
|
});
|
|
e.stopPropagation();
|
|
// prevent default action on ESC
|
|
e.preventDefault();
|
|
break;
|
|
case 38: // up
|
|
case 40: // down
|
|
case 37: // left
|
|
case 39: // right
|
|
case 33: // pgup
|
|
case 34: // pgdown
|
|
case 36: // home
|
|
case 35: // end
|
|
case 32: // space
|
|
case 107: // numpad [+]
|
|
case 109: // numpad [-]
|
|
case 106: // numpad [*]
|
|
e.stopPropagation();
|
|
break;
|
|
}
|
|
})).on('blur' + this._private.nameSpace, 'input[type=text]', this.proxy(function() {
|
|
if (!this.extSelectable || !this.extSelectable()) {
|
|
// cancel edit/save the changes
|
|
this.endEdit();
|
|
}
|
|
})).on('click' + this._private.nameSpace + ' dblclick' + this._private.nameSpace, 'input[type=text]', function(e) {
|
|
e.stopPropagation();
|
|
});
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extEditable()) {
|
|
this._editableInit();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// low level DOM functions
|
|
_editableDOM: {
|
|
// add edit field
|
|
add: function(item) {
|
|
var line = item.addClass('aciTreeEdited').children('.aciTreeLine');
|
|
line.find('.aciTreeText').html('<input id="aciTree-editable-tree-item" type="text" value="" style="-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;-o-user-select:text;user-select:text" />');
|
|
line.find('label').attr('for', 'aciTree-editable-tree-item');
|
|
this._editableDOM.get(item).val(this.getLabel(item));
|
|
},
|
|
// remove edit field
|
|
remove: function(item, label) {
|
|
var line = item.removeClass('aciTreeEdited').children('.aciTreeLine');
|
|
line.find('.aciTreeText').html(this.getLabel(item));
|
|
line.find('label').removeAttr('for');
|
|
},
|
|
// return edit field
|
|
get: function(item) {
|
|
return item ? item.children('.aciTreeLine').find('input[type=text]') : $([]);
|
|
}
|
|
},
|
|
// get edited item
|
|
edited: function() {
|
|
return this._instance.jQuery.find('.aciTreeEdited');
|
|
},
|
|
// test if item is edited
|
|
isEdited: function(item) {
|
|
return item && domApi.hasClass(item[0], 'aciTreeEdited');
|
|
},
|
|
// set focus to the input
|
|
_focusEdit: function(item) {
|
|
var field = this._editableDOM.get(item).focus().trigger('click')[0];
|
|
if (field) {
|
|
if (typeof field.selectionStart == 'number') {
|
|
field.selectionStart = field.selectionEnd = field.value.length;
|
|
} else if (field.createTextRange !== undefined) {
|
|
var range = field.createTextRange();
|
|
range.collapse(false);
|
|
range.select();
|
|
}
|
|
}
|
|
},
|
|
// override `setLabel`
|
|
setLabel: function(item, options) {
|
|
if (!this.extEditable() || !this.isEdited(item)) {
|
|
// call the parent
|
|
this._super(item, options);
|
|
}
|
|
},
|
|
// edit item inplace
|
|
edit: function(item, options) {
|
|
options = this._options(options, 'edit', 'editfail', 'wasedit', item);
|
|
if (this.extEditable() && this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeedit', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var edited = this.edited();
|
|
if (edited.length) {
|
|
if (edited[0] == item[0]) {
|
|
this._notify(item, options);
|
|
return;
|
|
} else {
|
|
this._editableDOM.remove.call(this, edited);
|
|
this._trigger(edited, 'endedit', options);
|
|
}
|
|
}
|
|
this._editableDOM.add.call(this, item);
|
|
this._focusEdit(item);
|
|
this._success(item, options);
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// end edit
|
|
// `options.save` when set to FALSE will not save the changes
|
|
endEdit: function(options) {
|
|
var item = this.edited();
|
|
options = this._options(options, 'edited', 'endeditfail', 'endedit', item);
|
|
if (this.extEditable() && this.isItem(item)) {
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforeendedit', options)) {
|
|
this._fail(item, options);
|
|
return;
|
|
}
|
|
var text = this._editableDOM.get(item).val();
|
|
this._editableDOM.remove.call(this, item);
|
|
if ((options.save === undefined) || options.save) {
|
|
this.setLabel(item, {
|
|
label: text
|
|
});
|
|
this._success(item, options);
|
|
} else {
|
|
this._notify(item, options);
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
},
|
|
// test if editable is enabled
|
|
extEditable: function() {
|
|
return this._instance.options.editable;
|
|
},
|
|
// override set `option`
|
|
option: function(option, value) {
|
|
if (this.wasInit() && !this.isLocked()) {
|
|
if ((option == 'editable') && (value != this.extEditable())) {
|
|
if (value) {
|
|
this._editableInit();
|
|
} else {
|
|
this._editableDone();
|
|
}
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(option, value);
|
|
},
|
|
// done editable
|
|
_editableDone: function() {
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
this._instance.jQuery.off(this._private.nameSpace, '.aciTreeItem');
|
|
this._instance.jQuery.off(this._private.nameSpace, 'input[type=text]');
|
|
var edited = this.edited();
|
|
if (edited.length) {
|
|
this.endEdit();
|
|
}
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._editableDone();
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
|
|
};
|
|
|
|
// extend the base aciTree class and add the editable stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_editable, 'aciTreeEditable');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
// for internal access
|
|
var domApi = aciPluginClass.plugins.aciTree_dom;
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds save/restore support for item states (open/selected) using local storage.
|
|
* The states are saved on item select/open and restored on treeview init.
|
|
* Require jStorage https://github.com/andris9/jStorage and the utils extension for finding items by ID.
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
persist: null // the storage key name to keep the states (should be unique/treeview)
|
|
};
|
|
|
|
// aciTree persist extension
|
|
// save/restore item state in/from local storage
|
|
|
|
var aciTree_persist = {
|
|
__extend: function() {
|
|
$.extend(this._private, {
|
|
// timeouts for the save operation
|
|
selectTimeout: null,
|
|
focusTimeout: null,
|
|
openTimeout: null
|
|
});
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// init persist
|
|
_initPersist: function() {
|
|
this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) {
|
|
if (options.uid == 'ui.persist') {
|
|
// skip processing itself
|
|
return;
|
|
}
|
|
switch (eventName) {
|
|
case 'init':
|
|
api._persistRestore();
|
|
break;
|
|
case 'selected':
|
|
case 'deselected':
|
|
// support `selectable` extension
|
|
api._persistLater('selected');
|
|
break;
|
|
case 'focus':
|
|
case 'blur':
|
|
// support `selectable` extension
|
|
api._persistLater('focused');
|
|
break;
|
|
case 'opened':
|
|
case 'closed':
|
|
api._persistLater('opened');
|
|
break;
|
|
}
|
|
});
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extPersist()) {
|
|
this._initPersist();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// persist states
|
|
_persistLater: function(type) {
|
|
switch (type) {
|
|
case 'selected':
|
|
window.clearTimeout(this._private.selectTimeout);
|
|
this._private.selectTimeout = window.setTimeout(this.proxy(function() {
|
|
this._persistSelected();
|
|
}), 250);
|
|
break;
|
|
case 'focused':
|
|
window.clearTimeout(this._private.focusTimeout);
|
|
this._private.focusTimeout = window.setTimeout(this.proxy(function() {
|
|
this._persistFocused();
|
|
}), 250);
|
|
break;
|
|
case 'opened':
|
|
window.clearTimeout(this._private.openTimeout);
|
|
this._private.openTimeout = window.setTimeout(this.proxy(function() {
|
|
this._persistOpened();
|
|
}), 250);
|
|
break;
|
|
}
|
|
},
|
|
// restore item states
|
|
_persistRestore: function() {
|
|
var queue = new this._queue(this, this._instance.options.queue);
|
|
var task = new this._task(queue, function(complete) {
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable()) {
|
|
var selected = $.jStorage.get('aciTree_' + this._instance.options.persist + '_selected');
|
|
if (selected instanceof Array) {
|
|
// select all saved items
|
|
for (var i in selected) {
|
|
(function(path) {
|
|
queue.push(function(complete) {
|
|
this.searchPath(null, {
|
|
success: function(item) {
|
|
this.select(item, {
|
|
uid: 'ui.persist',
|
|
success: function() {
|
|
complete();
|
|
},
|
|
fail: complete,
|
|
focus: false
|
|
});
|
|
},
|
|
fail: complete,
|
|
path: path.split(';')
|
|
});
|
|
});
|
|
})(selected[i]);
|
|
if (!this._instance.options.multiSelectable) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var focused = $.jStorage.get('aciTree_' + this._instance.options.persist + '_focused');
|
|
if (focused instanceof Array) {
|
|
// focus all saved items
|
|
for (var i in focused) {
|
|
(function(path) {
|
|
queue.push(function(complete) {
|
|
this.searchPath(null, {
|
|
success: function(item) {
|
|
this.focus(item, {
|
|
uid: 'ui.persist',
|
|
success: function(item) {
|
|
this.setVisible(item, {
|
|
center: true
|
|
});
|
|
complete();
|
|
},
|
|
fail: complete
|
|
});
|
|
},
|
|
fail: complete,
|
|
path: path.split(';')
|
|
});
|
|
});
|
|
})(focused[i]);
|
|
}
|
|
}
|
|
}
|
|
complete();
|
|
});
|
|
var opened = $.jStorage.get('aciTree_' + this._instance.options.persist + '_opened');
|
|
if (opened instanceof Array) {
|
|
// open all saved items
|
|
for (var i in opened) {
|
|
(function(path) {
|
|
// add item to queue
|
|
task.push(function(complete) {
|
|
this.searchPath(null, {
|
|
success: function(item) {
|
|
this.open(item, {
|
|
uid: 'ui.persist',
|
|
success: complete,
|
|
fail: complete
|
|
});
|
|
},
|
|
fail: complete,
|
|
path: path.split(';'),
|
|
load: true
|
|
});
|
|
});
|
|
})(opened[i]);
|
|
}
|
|
}
|
|
},
|
|
// persist selected items
|
|
_persistSelected: function() {
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable()) {
|
|
var selected = [];
|
|
this.selected().each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
var path = this.pathId(item);
|
|
path.push(this.getId(item));
|
|
selected.push(path.join(';'));
|
|
}, true));
|
|
$.jStorage.set('aciTree_' + this._instance.options.persist + '_selected', selected);
|
|
}
|
|
},
|
|
// persist focused item
|
|
_persistFocused: function() {
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable()) {
|
|
var focused = [];
|
|
this.focused().each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
var path = this.pathId(item);
|
|
path.push(this.getId(item));
|
|
focused.push(path.join(';'));
|
|
}, true));
|
|
$.jStorage.set('aciTree_' + this._instance.options.persist + '_focused', focused);
|
|
}
|
|
},
|
|
// persist opened items
|
|
_persistOpened: function() {
|
|
var opened = [];
|
|
this.inodes(this.children(null, true), true).each(this.proxy(function(element) {
|
|
var item = $(element);
|
|
if (this.isOpenPath(item)) {
|
|
var path = this.pathId(item);
|
|
path.push(this.getId(item));
|
|
opened.push(path.join(';'));
|
|
}
|
|
}, true));
|
|
$.jStorage.set('aciTree_' + this._instance.options.persist + '_opened', opened);
|
|
},
|
|
// test if there is any saved data
|
|
isPersist: function() {
|
|
if (this.extPersist()) {
|
|
var selected = $.jStorage.get('aciTree_' + this._instance.options.persist + '_selected');
|
|
if (selected instanceof Array) {
|
|
return true;
|
|
}
|
|
var focused = $.jStorage.get('aciTree_' + this._instance.options.persist + '_focused');
|
|
if (focused instanceof Array) {
|
|
return true;
|
|
}
|
|
var opened = $.jStorage.get('aciTree_' + this._instance.options.persist + '_opened');
|
|
if (opened instanceof Array) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
// remove any saved states
|
|
unpersist: function() {
|
|
if (this.extPersist()) {
|
|
$.jStorage.deleteKey('aciTree_' + this._instance.options.persist + '_selected');
|
|
$.jStorage.deleteKey('aciTree_' + this._instance.options.persist + '_focused');
|
|
$.jStorage.deleteKey('aciTree_' + this._instance.options.persist + '_opened');
|
|
}
|
|
},
|
|
// test if persist is enabled
|
|
extPersist: function() {
|
|
return this._instance.options.persist;
|
|
},
|
|
// override set `option`
|
|
option: function(option, value) {
|
|
var persist = this.extPersist();
|
|
// call the parent
|
|
this._super(option, value);
|
|
if (this.extPersist() != persist) {
|
|
if (persist) {
|
|
this._donePersist();
|
|
} else {
|
|
this._initPersist();
|
|
}
|
|
}
|
|
},
|
|
// done persist
|
|
_donePersist: function() {
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._donePersist();
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
};
|
|
|
|
// extend the base aciTree class and add the persist stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_persist, 'aciTreePersist');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds hash/fragment support using aciFragment, it opens/select item(s) based on variables stored in the fragment part of the URL.
|
|
* The states are loaded from the URL fragment and set on treeview init. Multiple item IDs separated with ";" are supported for
|
|
* opening/selecting deep items (if loading nodes is required).
|
|
* Require aciFragment https://github.com/dragosu/jquery-aciFragment and the utils extension for finding items by ID.
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
selectHash: null, // hash key name to select a item (item path IDs as key value, multiple item IDs separated with a ";")
|
|
openHash: null // hash key name to open item(s) (item path IDs as key value, multiple item IDs separated with a ";")
|
|
};
|
|
|
|
// aciTree hash extension
|
|
// select/open items based on IDs stored in the fragment of the current URL
|
|
|
|
var aciTree_hash = {
|
|
__extend: function() {
|
|
$.extend(this._private, {
|
|
lastSelect: null,
|
|
lastOpen: null,
|
|
// store `aciFragment` api
|
|
hashApi: null
|
|
});
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// init hash
|
|
_hashInit: function() {
|
|
// init `aciFragment`
|
|
this._instance.jQuery.aciFragment();
|
|
this._private.hashApi = this._instance.jQuery.aciFragment('api');
|
|
this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) {
|
|
switch (eventName) {
|
|
case 'init':
|
|
api._hashRestore();
|
|
break;
|
|
}
|
|
}).bind('acifragment' + this._private.nameSpace, this.proxy(function(event, api, anchorChanged) {
|
|
event.stopPropagation();
|
|
this._hashRestore();
|
|
}));
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extHast()) {
|
|
this._hashInit();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// restore item states from hash
|
|
_hashRestore: function() {
|
|
var queue = this._instance.queue;
|
|
var process = function(opened) {
|
|
// open all hash items
|
|
for (var i in opened) {
|
|
(function(id) {
|
|
// add item to queue
|
|
queue.push(function(complete) {
|
|
this.search(null, {
|
|
success: function(item) {
|
|
this.open(item, {
|
|
uid: 'ui.hash',
|
|
success: complete,
|
|
fail: complete
|
|
});
|
|
},
|
|
fail: complete,
|
|
search: id
|
|
});
|
|
});
|
|
})(opened[i]);
|
|
}
|
|
};
|
|
if (this._instance.options.openHash) {
|
|
var hash = this._private.hashApi.get(this._instance.options.openHash, '');
|
|
if (hash.length && (hash != this._private.lastOpen)) {
|
|
this._private.lastOpen = hash;
|
|
var opened = hash.split(';');
|
|
process(opened);
|
|
}
|
|
}
|
|
// support `selectable` extension
|
|
if (this._instance.options.selectHash && this.extSelectable && this.extSelectable()) {
|
|
var hash = this._private.hashApi.get(this._instance.options.selectHash, '');
|
|
if (hash.length && (hash != this._private.lastSelect)) {
|
|
this._private.lastSelect = hash;
|
|
var opened = hash.split(';');
|
|
var selected = opened.pop();
|
|
process(opened);
|
|
if (selected) {
|
|
// select item
|
|
queue.push(function(complete) {
|
|
this.search(null, {
|
|
success: function(item) {
|
|
this.select(item, {
|
|
uid: 'ui.hash',
|
|
success: function(item) {
|
|
this.setVisible(item, {
|
|
center: true
|
|
});
|
|
complete();
|
|
},
|
|
fail: complete
|
|
});
|
|
},
|
|
fail: complete,
|
|
search: selected
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// test if hash is enabled
|
|
extHast: function() {
|
|
return this._instance.options.selectHash || this._instance.options.openHash;
|
|
},
|
|
// override set option
|
|
option: function(option, value) {
|
|
var hash = this.extHast();
|
|
// call the parent
|
|
this._super(option, value);
|
|
if (this.extHast() != hash) {
|
|
if (hash) {
|
|
this._hashDone();
|
|
} else {
|
|
this._hashInit();
|
|
}
|
|
}
|
|
},
|
|
// done hash
|
|
_hashDone: function() {
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
this._private.hashApi = null;
|
|
this._instance.jQuery.aciFragment('destroy');
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._hashDone();
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
};
|
|
|
|
// extend the base aciTree class and add the hash stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_hash, 'aciTreeHash');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
})(jQuery, this);
|
|
|
|
/*
|
|
* aciTree jQuery Plugin v4.5.0-rc.7
|
|
* http://acoderinsights.ro
|
|
*
|
|
* Copyright (c) 2014 Dragos Ursu
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
*
|
|
* Require jQuery Library >= v1.9.0 http://jquery.com
|
|
* + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin
|
|
*/
|
|
|
|
/*
|
|
* This extension adds the possibility to sort the tree items.
|
|
* Require aciSortable https://github.com/dragosu/jquery-aciSortable and the utils extension for reordering items.
|
|
*/
|
|
|
|
(function($, window, undefined) {
|
|
|
|
// extra default options
|
|
|
|
var options = {
|
|
sortable: false, // if TRUE then the tree items can be sorted
|
|
sortDelay: 750, // how many [ms] before opening a inode on hovering when in drag
|
|
// called by the `aciSortable` inside the `drag` callback
|
|
sortDrag: function(item, placeholder, isValid, helper) {
|
|
if (!isValid) {
|
|
var move = this.getLabel(item);
|
|
if (this._private.dragDrop && (this._private.dragDrop.length > 1)) {
|
|
move += ' and #' + (this._private.dragDrop.length - 1) + ' more';
|
|
}
|
|
helper.html(move);
|
|
}
|
|
},
|
|
// called by the `aciSortable` inside the `valid` callback
|
|
sortValid: function(item, hover, before, isContainer, placeholder, helper) {
|
|
var move = this.getLabel(item);
|
|
if (this._private.dragDrop.length > 1) {
|
|
move += ' and #' + (this._private.dragDrop.length - 1) + ' more';
|
|
}
|
|
if (isContainer) {
|
|
helper.html('move ' + move + ' to ' + this.getLabel(this.itemFrom(hover)));
|
|
placeholder.removeClass('aciTreeAfter aciTreeBefore');
|
|
} else if (before !== null) {
|
|
if (before) {
|
|
helper.html('move ' + move + ' before ' + this.getLabel(hover));
|
|
placeholder.removeClass('aciTreeAfter').addClass('aciTreeBefore');
|
|
} else {
|
|
helper.html('move ' + move + ' after ' + this.getLabel(hover));
|
|
placeholder.removeClass('aciTreeBefore').addClass('aciTreeAfter');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// aciTree sortable extension
|
|
|
|
var aciTree_sortable = {
|
|
__extend: function() {
|
|
// add extra data
|
|
$.extend(this._private, {
|
|
openTimeout: null,
|
|
dragDrop: null // the items used in drag & drop
|
|
});
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// init sortable
|
|
_sortableInit: function() {
|
|
this._instance.jQuery.aciSortable({
|
|
container: '.aciTreeUl',
|
|
item: '.aciTreeLi',
|
|
child: 50,
|
|
childHolder: '<ul class="aciTreeUl aciTreeChild"></ul>',
|
|
childHolderSelector: '.aciTreeChild',
|
|
placeholder: '<li class="aciTreeLi aciTreePlaceholder"><div></div></li>',
|
|
placeholderSelector: '.aciTreePlaceholder',
|
|
helper: '<div class="aciTreeHelper"></div>',
|
|
helperSelector: '.aciTreeHelper',
|
|
// just before drag start
|
|
before: this.proxy(function(item) {
|
|
// init before drag
|
|
if (!this._initDrag(item)) {
|
|
return false;
|
|
}
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'beforedrag')) {
|
|
this._trigger(item, 'dragfail');
|
|
return false;
|
|
}
|
|
return true;
|
|
}),
|
|
// just after drag start, before dragging
|
|
start: this.proxy(function(item, placeholder, helper) {
|
|
this._instance.jQuery.addClass('aciTreeDragDrop');
|
|
helper.stop(true).css('opacity', 1);
|
|
}),
|
|
// when in drag
|
|
drag: this.proxy(function(item, placeholder, isValid, helper) {
|
|
if (!isValid) {
|
|
window.clearTimeout(this._private.openTimeout);
|
|
}
|
|
if (this._instance.options.sortDrag) {
|
|
this._instance.options.sortDrag.apply(this, arguments);
|
|
}
|
|
}),
|
|
// to check the drop target (when the placeholder is repositioned)
|
|
valid: this.proxy(function(item, hover, before, isContainer, placeholder, helper) {
|
|
window.clearTimeout(this._private.openTimeout);
|
|
if (!this._checkDrop(item, hover, before, isContainer, placeholder, helper)) {
|
|
return false;
|
|
}
|
|
var options = this._options({
|
|
hover: hover,
|
|
before: before,
|
|
isContainer: isContainer,
|
|
placeholder: placeholder,
|
|
helper: helper
|
|
});
|
|
// a way to cancel the operation
|
|
if (!this._trigger(item, 'checkdrop', options)) {
|
|
return false;
|
|
}
|
|
if (this.isInode(hover) && !this.isOpen(hover)) {
|
|
this._private.openTimeout = window.setTimeout(this.proxy(function() {
|
|
this.open(hover);
|
|
}), this._instance.options.sortDelay);
|
|
}
|
|
if (this._instance.options.sortValid) {
|
|
this._instance.options.sortValid.apply(this, arguments);
|
|
}
|
|
return true;
|
|
}),
|
|
// when dragged as child
|
|
create: this.proxy(function(api, item, hover) {
|
|
if (this.isLeaf(hover)) {
|
|
hover.append(api._instance.options.childHolder);
|
|
return true;
|
|
}
|
|
return false;
|
|
}, true),
|
|
// on drag end
|
|
end: this.proxy(function(item, hover, placeholder, helper) {
|
|
window.clearTimeout(this._private.openTimeout);
|
|
var options = {
|
|
placeholder: placeholder,
|
|
helper: helper
|
|
};
|
|
options = this._options(options, 'sorted', 'dropfail', null, item);
|
|
if (placeholder.parent().length) {
|
|
var prev = this.prev(placeholder, true);
|
|
if (prev.length) {
|
|
// add after a item
|
|
placeholder.detach();
|
|
var items = $(this._private.dragDrop.get().reverse());
|
|
this._private.dragDrop = null;
|
|
items.each(this.proxy(function(element) {
|
|
this.moveAfter($(element), this._inner(options, {
|
|
success: options.success,
|
|
fail: options.fail,
|
|
after: prev
|
|
}));
|
|
}, true));
|
|
} else {
|
|
var next = this.next(placeholder, true);
|
|
if (next.length) {
|
|
// add before a item
|
|
placeholder.detach();
|
|
var items = $(this._private.dragDrop.get().reverse());
|
|
this._private.dragDrop = null;
|
|
items.each(this.proxy(function(element) {
|
|
this.moveBefore($(element), this._inner(options, {
|
|
success: options.success,
|
|
fail: options.fail,
|
|
before: next
|
|
}));
|
|
}, true));
|
|
} else {
|
|
// add as a child
|
|
var parent = this.parent(placeholder);
|
|
var container = placeholder.parent();
|
|
placeholder.detach();
|
|
container.remove();
|
|
if (this.isLeaf(parent)) {
|
|
// we can set asChild only for leaves
|
|
var items = this._private.dragDrop;
|
|
this.asChild(items.eq(0), this._inner(options, {
|
|
success: function() {
|
|
this._success(item, options);
|
|
this.open(parent);
|
|
items.filter(':gt(0)').each(this.proxy(function(element) {
|
|
this.moveAfter($(element), this._inner(options, {
|
|
success: options.success,
|
|
fail: options.fail,
|
|
after: this.last(parent)
|
|
}));
|
|
}, true));
|
|
},
|
|
fail: options.fail,
|
|
parent: parent
|
|
}));
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this._fail(item, options);
|
|
}
|
|
this._private.dragDrop = null;
|
|
if (helper.parent().length) {
|
|
// the helper is inserted in the DOM
|
|
var top = $(window).scrollTop();
|
|
var left = $(window).scrollLeft();
|
|
var rect = item[0].getBoundingClientRect();
|
|
// animate helper to item position
|
|
helper.animate({
|
|
top: rect.top + top,
|
|
left: rect.left + left,
|
|
opacity: 0
|
|
},
|
|
{
|
|
complete: function() {
|
|
// detach the helper when completed
|
|
helper.detach();
|
|
}
|
|
});
|
|
}
|
|
this._instance.jQuery.removeClass('aciTreeDragDrop');
|
|
})
|
|
});
|
|
},
|
|
// override `_initHook`
|
|
_initHook: function() {
|
|
if (this.extSortable()) {
|
|
this._sortableInit();
|
|
}
|
|
// call the parent
|
|
this._super();
|
|
},
|
|
// reduce items by removing the childrens
|
|
_parents: function(items) {
|
|
var len = items.length, a, b, remove = [];
|
|
for (var i = 0; i < len - 1; i++) {
|
|
a = items.eq(i);
|
|
for (var j = i + 1; j < len; j++) {
|
|
b = items.eq(j);
|
|
if (this.isChildren(a, b)) {
|
|
remove.push(items[j]);
|
|
} else if (this.isChildren(b, a)) {
|
|
remove.push(items[i]);
|
|
}
|
|
}
|
|
}
|
|
return items.not(remove);
|
|
},
|
|
// called before drag start
|
|
_initDrag: function(item) {
|
|
// support `selectable` extension
|
|
if (this.extSelectable && this.extSelectable()) {
|
|
if (!this.hasFocus()) {
|
|
this._instance.jQuery.focus();
|
|
}
|
|
if (!this.isEnabled(item)) {
|
|
return false;
|
|
}
|
|
var drag = this.selected();
|
|
if (drag.length) {
|
|
if (!this.isSelected(item)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
drag = item;
|
|
}
|
|
this._private.dragDrop = this._parents(drag);
|
|
} else {
|
|
this._instance.jQuery.focus();
|
|
this._private.dragDrop = item;
|
|
}
|
|
return true;
|
|
},
|
|
// check the drop target
|
|
_checkDrop: function(item, hover, before, isContainer, placeholder, helper) {
|
|
var items = this._private.dragDrop;
|
|
if (!items) {
|
|
return false;
|
|
}
|
|
var test = this.itemFrom(hover);
|
|
if (items.is(test) || items.has(test[0]).length) {
|
|
return false;
|
|
}
|
|
if (!isContainer) {
|
|
test = before ? this.prev(hover) : this.next(hover);
|
|
if (items.is(test)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
// test if sortable is enabled
|
|
extSortable: function() {
|
|
return this._instance.options.sortable;
|
|
},
|
|
// override set `option`
|
|
option: function(option, value) {
|
|
if (this.wasInit() && !this.isLocked()) {
|
|
if ((option == 'sortable') && (value != this.extSortable())) {
|
|
if (value) {
|
|
this._sortableInit();
|
|
} else {
|
|
this._sortableDone();
|
|
}
|
|
}
|
|
}
|
|
// call the parent
|
|
this._super(option, value);
|
|
},
|
|
// done sortable
|
|
_sortableDone: function() {
|
|
this._instance.jQuery.unbind(this._private.nameSpace);
|
|
this._instance.jQuery.aciSortable('destroy');
|
|
},
|
|
// override `_destroyHook`
|
|
_destroyHook: function(unloaded) {
|
|
if (unloaded) {
|
|
this._sortableDone();
|
|
}
|
|
// call the parent
|
|
this._super(unloaded);
|
|
}
|
|
};
|
|
|
|
// extend the base aciTree class and add the sortable stuff
|
|
aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_sortable, 'aciTreeSortable');
|
|
|
|
// add extra default options
|
|
aciPluginClass.defaults('aciTree', options);
|
|
|
|
})(jQuery, this);
|