OpenXE/www/js/aciTree/js/jquery.aciTree.utils.js
2021-05-21 08:49:41 +02:00

747 lines
32 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
*/
/*
* 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);