import * as _ from 'lodash';
import { collectionToModel } from './lib/collection-to-model';
import { EventEmitter2 } from 'eventemitter2';
import { objectToNode } from './lib/object-to-node';
import { Promise } from 'es6-promise';
import { standardizePromise } from './lib/standardize-promise';
import TreeNode from './treenode';
import TreeNodes from './treenodes';
import uuidV4 from 'uuid/v4';
/**
* Maps a method to the root TreeNodes collection.
*
* @private
* @param {InspireTree} tree Tree instance.
* @param {string} method Method name.
* @param {arguments} args Proxied arguments.
* @return {mixed} Proxied return value.
*/
function map(tree, method, args) {
return tree.model[method].apply(tree.model, args);
}
/**
* Represents a singe tree instance.
*
* @return {InspireTree} Tree instance.
*/
class InspireTree extends EventEmitter2 {
constructor(opts) {
super();
// Init properties
this._lastSelectedNode;
this._muted = false;
this.allowsLoadEvents = false;
this.id = uuidV4();
this.initialized = false;
this.isDynamic = false;
this.opts = opts;
this.preventDeselection = false;
// Assign defaults
this.config = _.defaultsDeep({}, opts, {
allowLoadEvents: [],
checkbox: {
autoCheckChildren: true
},
contextMenu: false,
data: false,
editable: false,
editing: {
add: false,
edit: false,
remove: false
},
nodes: {
resetStateOnRestore: true
},
pagination: {
limit: -1
},
search: {
matcher: false,
matchProcessor: false
},
selection: {
allow: _.noop,
autoDeselect: true,
autoSelectChildren: false,
disableDirectDeselection: false,
mode: 'default',
multiple: false,
require: false
},
showCheckboxes: false,
sort: false
});
// If checkbox mode, we must force auto-selecting children
if (this.config.selection.mode === 'checkbox') {
this.config.selection.autoSelectChildren = true;
// In checkbox mode, checked=selected
this.on('node.checked', node => {
if (!node.selected()) {
node.select(true);
}
});
this.on('node.selected', node => {
if (!node.checked()) {
node.check(true);
}
});
this.on('node.unchecked', node => {
if (node.selected()) {
node.deselect(true);
}
});
this.on('node.deselected', node => {
if (node.checked()) {
node.uncheck(true);
}
});
}
// If auto-selecting children, we must force multiselect
if (this.config.selection.autoSelectChildren) {
this.config.selection.multiple = true;
this.config.selection.autoDeselect = false;
}
// Treat editable as full edit mode
if (opts.editable && !opts.editing) {
this.config.editing.add = true;
this.config.editing.edit = true;
this.config.editing.remove = true;
}
// Support simple config for search
if (_.isFunction(opts.search)) {
this.config.search = {
matcher: opts.search,
matchProcessor: false
};
}
// Init the default state for nodes
this.defaultState = {
collapsed: true,
editable: _.get(this, 'config.editing.edit'),
editing: false,
draggable: true,
'drop-target': true,
focused: false,
hidden: false,
indeterminate: false,
loading: false,
matched: false,
removed: false,
rendered: false,
selectable: true,
selected: false
};
// Cache some configs
this.allowsLoadEvents = _.isArray(this.config.allowLoadEvents) && this.config.allowLoadEvents.length > 0;
this.isDynamic = _.isFunction(this.config.data);
// Override emitter so we can better control flow
const emit = this.emit;
this.emit = (...args) => {
if (!this.isEventMuted(args[0])) {
// Duck-type for a DOM event
if (_.isFunction(_.get(args, '[0].preventDefault'))) {
const event = args[0];
event.treeDefaultPrevented = false;
event.preventTreeDefault = () => {
event.treeDefaultPrevented = true;
};
}
emit.apply(this, args);
}
};
// Init the model
this.model = new TreeNodes(this);
// Load initial user data
if (this.config.data) {
this.load(this.config.data);
}
this.initialized = true;
}
/**
* Adds a new node. If a sort method is configured,
* the node will be added in the appropriate order.
*
* @param {object} node Node
* @return {TreeNode} Node object.
*/
addNode() {
return map(this, 'addNode', arguments);
}
/**
* Add nodes.
*
* @param {array} nodes Array of node objects.
* @return {TreeNodes} Added node objects.
*/
addNodes(nodes) {
this.batch();
const newNodes = new TreeNodes(this);
_.each(nodes, node => newNodes.push(this.addNode(node)));
this.end();
return newNodes;
}
/**
* Release pending data changes to any listeners.
*
* Will skip rendering as long as any calls
* to `batch` have yet to be resolved,
*
* @private
* @return {void}
*/
applyChanges() {
return this.model.applyChanges();
}
/**
* Query for all available nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
available() {
return map(this, 'available', arguments);
}
/**
* Batch multiple changes for listeners (i.e. DOM)
*
* @private
* @return {void}
*/
batch() {
return this.model.batch();
}
/**
* Blur children in this collection.
*
* @return {TreeNodes} Array of node objects.
*/
blur() {
return map(this, 'blur', arguments);
}
/**
* Blur (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
blurDeep() {
return map(this, 'blurDeep', arguments);
}
/**
* Compares any number of TreeNode objects and returns
* the minimum and maximum (starting/ending) nodes.
*
* @return {array} Array with two TreeNode objects.
*/
boundingNodes() {
const pathMap = _.transform(arguments, (col, node) => {
col[node.indexPath().replace(/\./g, '')] = node;
}, {});
const [head, ...tail] = _.sortBy(Object.keys(pathMap));
return [
_.get(pathMap, head),
_.get(pathMap, tail)
];
}
/**
* Check if the tree will auto-deselect currently selected nodes
* when a new selection is made.
*
* @return {boolean} If tree will auto-deselect nodes.
*/
canAutoDeselect() {
return this.config.selection.autoDeselect && !this.preventDeselection;
}
/**
* Query for all checked nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
checked() {
return map(this, 'checked', arguments);
}
/**
* Clean nodes.
*
* @return {TreeNodes} Array of node objects.
*/
clean() {
return map(this, 'clean', arguments);
}
/**
* Clear nodes matched by previous search, restore all nodes and collapse parents.
*
* @return {Tree} Tree instance.
*/
clearSearch() {
this.batch();
this.recurseDown(node => {
// Reset search effects (show node, collapse, reset matched)
node.show().collapse().state('matched', false);
});
this.end();
return this;
}
/**
* Clones (deeply) the array of nodes.
*
* Note: Cloning will *not* clone the context pointer.
*
* @return {TreeNodes} Array of cloned nodes.
*/
clone() {
return map(this, 'clone', arguments);
}
/**
* Collapse nodes.
*
* @return {TreeNodes} Array of node objects.
*/
collapse() {
return map(this, 'collapse', arguments);
}
/**
* Query for all collapsed nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
collapsed() {
return map(this, 'collapsed', arguments);
}
/**
* Collapse (deeply) all children.
*
* @return {TreeNodes} Array of node objects.
*/
collapseDeep() {
return map(this, 'collapseDeep', arguments);
}
/**
* Concat multiple TreeNodes arrays.
*
* @param {TreeNodes} nodes Array of nodes.
* @return {TreeNodes} Resulting node array.
*/
concat() {
return map(this, 'concat', arguments);
}
/**
* Copy nodes to another tree instance.
*
* @param {object} dest Destination Inspire Tree.
* @param {boolean} hierarchy Include necessary ancestors to match hierarchy.
* @param {boolean} includeState Include itree.state object.
* @return {object} Methods to perform action on copied nodes.
*/
copy() {
return map(this, 'copy', arguments);
}
/**
* Creates a TreeNode without adding it. If the obj is already a TreeNode it's returned without modification.
*
* @param {object} obj Source node object.
* @return {TreeNode} Node object.
*/
createNode(obj) {
return InspireTree.isTreeNode(obj) ? obj : objectToNode(this, obj);
}
/**
* Return deepest nodes.
*
* @return {TreeNodes} Array of node objects.
*/
deepest() {
return map(this, 'deepest', arguments);
}
/**
* Deselect nodes.
*
* @return {TreeNodes} Array of node objects.
*/
deselect() {
return map(this, 'deselect', arguments);
}
/**
* Deselect (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
deselectDeep() {
return map(this, 'deselectDeep', arguments);
}
/**
* Disable auto-deselection of currently selected nodes.
*
* @return {Tree} Tree instance.
*/
disableDeselection() {
if (this.config.selection.multiple) {
this.preventDeselection = true;
}
return this;
}
/**
* Iterate each TreeNode.
*
* @param {function} iteratee Iteratee invoke for each node.
* @return {TreeNodes} Array of node objects.
*/
each() {
return map(this, 'each', arguments);
}
/**
* Query for all editable nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
editable() {
return map(this, 'editable', arguments);
}
/**
* Query for all nodes in editing mode.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
editing() {
return map(this, 'editing', arguments);
}
/**
* Enable auto-deselection of currently selected nodes.
*
* @return {Tree} Tree instance.
*/
enableDeselection() {
this.preventDeselection = false;
return this;
}
/**
* Release the current batch.
*
* @private
* @return {void}
*/
end() {
return this.model.end();
}
/**
* Check if every node passes the given test.
*
* @param {function} tester Test each node in this collection,
* @return {boolean} True if every node passes the test.
*/
every() {
return map(this, 'every', arguments);
}
/**
* Expand children.
*
* @return {TreeNodes} Array of node objects.
*/
expand() {
return map(this, 'expand', arguments);
}
/**
* Expand (deeply) all nodes.
*
* @return {Promise<TreeNodes>} Promise resolved when all children have loaded and expanded.
*/
expandDeep() {
return map(this, 'expandDeep', arguments);
}
/**
* Query for all expanded nodes.
*
* @return {TreeNodes} Array of node objects.
*/
expanded() {
return map(this, 'expanded', arguments);
}
/**
* Clone a hierarchy of all nodes matching a predicate.
*
* Because it filters deeply, we must clone all nodes so that we
* don't affect the actual node array.
*
* @param {string|function} predicate State flag or custom function.
* @return {TreeNodes} Array of node objects.
*/
extract() {
return map(this, 'extract', arguments);
}
/**
* Filter all nodes matching the given predicate.
*
* @param {function} predicate Test function.
* @return {TreeNodes} Array of node objects.
*/
filter() {
return map(this, 'filter', arguments);
}
/**
* Filter all nodes matching the given predicate.
*
* @param {string|function} predicate State flag or custom function.
* @return {TreeNodes} Array of node objects.
*/
filterBy() {
return map(this, 'filterBy', arguments);
}
/**
* Returns the first node matching predicate.
*
* @param {function} predicate Predicate function, accepts a single node and returns a boolean.
* @return {TreeNode} First matching TreeNode, or undefined.
*/
find() {
return map(this, 'find', arguments);
}
/**
* Returns the first shallow node matching predicate.
*
* @param {function} predicate Predicate function, accepts a single node and returns a boolean.
* @return {TreeNode} First matching TreeNode, or undefined.
*/
first() {
return map(this, 'first', arguments);
}
/**
* Flatten and get only node(s) matching the expected state or predicate function.
*
* @param {string|function} predicate State property or custom function.
* @return {TreeNodes} Flat array of matching nodes.
*/
flatten() {
return map(this, 'flatten', arguments);
}
/**
* Query for all focused nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
focused() {
return map(this, 'focused', arguments);
}
/**
* Iterate each TreeNode.
*
* @param {function} iteratee Iteratee invoke for each node.
* @return {TreeNodes} Array of node objects.
*/
forEach() {
return map(this, 'each', arguments);
}
/**
* Get a specific node by its index, or undefined if it doesn't exist.
*
* @param {int} index Numeric index of requested node.
* @return {TreeNode} Node object. Undefined if invalid index.
*/
get() {
return map(this, 'get', arguments);
}
/**
* Query for all hidden nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
hidden() {
return map(this, 'hidden', arguments);
}
/**
* Hide nodes.
*
* @return {TreeNodes} Array of node objects.
*/
hide() {
return map(this, 'hide', arguments);
}
/**
* Hide (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
hideDeep() {
return map(this, 'hideDeep', arguments);
}
/**
* Query for all indeterminate nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
indeterminate() {
return map(this, 'indeterminate', arguments);
}
/**
* Get the index of the given node.
*
* @param {TreeNode} node Root tree node.
* @return {int} Index of the node.
*/
indexOf() {
return map(this, 'indexOf', arguments);
}
/**
* Insert a new node at the given position.
*
* @param {integer} index Index at which to insert the node.
* @param {object} object Raw node object or TreeNode.
* @return {TreeNode} Node object.
*/
insertAt() {
return map(this, 'insertAt', arguments);
}
/**
* Invoke method(s) on each node.
*
* @param {string|array} methods Method name(s).
* @return {TreeNodes} Array of node objects.
*/
invoke() {
return map(this, 'invoke', arguments);
}
/**
* Invoke method(s) deeply.
*
* @param {string|array} methods Method name(s).
* @return {TreeNodes} Array of node objects.
*/
invokeDeep() {
return map(this, 'invokeDeep', arguments);
}
/**
* Check if an event is currently muted.
*
* @param {string} eventName Event name.
* @return {boolean} If event is muted.
*/
isEventMuted(eventName) {
if (_.isBoolean(this.muted())) {
return this.muted();
}
return _.includes(this.muted(), eventName);
}
/**
* Check if an object is a Tree.
*
* @param {object} object Object
* @return {boolean} If object is a Tree.
*/
isTree(object) {
return (object instanceof InspireTree);
}
/**
* Check if an object is a TreeNode.
*
* @param {object} obj Object
* @return {boolean} If object is a TreeNode.
*/
static isTreeNode(obj) {
return obj instanceof TreeNode;
}
/**
* Check if an object is a TreeNodes array.
*
* @param {object} obj Object
* @return {boolean} If object is a TreeNodes array.
*/
static isTreeNodes(obj) {
return obj instanceof TreeNodes;
}
/**
* Join nodes into a resulting string.
*
* @param {string} separator Separator, defaults to a comma
* @return {string} Strings from root node objects.
*/
join() {
return map(this, 'join', arguments);
}
/**
* Returns the last shallow node matching predicate.
*
* @param {function} predicate Predicate function, accepts a single node and returns a boolean.
* @return {TreeNode} Last matching shallow TreeNode, or undefined.
*/
last() {
return map(this, 'last', arguments);
}
/**
* Get the most recently selected node, if any.
*
* @return {TreeNode} Last selected node, or undefined.
*/
lastSelectedNode() {
return this._lastSelectedNode;
}
/**
* Load data. Accepts an array, function, or promise.
*
* @param {array|function|Promise} loader Array of nodes, function, or promise resolving an array of nodes.
* @return {Promise<TreeNodes>} Promise resolved upon successful load, rejected on error.
* @example
*
* tree.load($.getJSON('nodes.json'));
*/
load(loader) {
const promise = new Promise((resolve, reject) => {
const complete = (nodes, totalNodes) => {
// A little type-safety for silly situations
if (!_.isArrayLike(nodes)) {
return reject(new TypeError('Loader requires an array-like `nodes` parameter.'));
}
// Delay event for synchronous loader. Otherwise it fires
// before the user has a chance to listen.
if (!this.initialized && _.isArrayLike(nodes)) {
setTimeout(() => {
this.emit('data.loaded', nodes);
});
}
else {
this.emit('data.loaded', nodes);
}
// Parse newly-loaded nodes
const newModel = collectionToModel(this, nodes);
// Concat only if loading is deferred
if (this.config.deferredLoading) {
this.model = this.model.concat(newModel);
}
else {
this.model = newModel;
}
// Set pagination
this.model._pagination.total = nodes.length;
if (_.parseInt(totalNodes) > nodes.length) {
this.model._pagination.total = _.parseInt(totalNodes);
}
// Set pagination totals if resolver failed to provide them
if (!totalNodes) {
this.model.recurseDown(node => {
if (node.hasChildren()) {
node.children._pagination.total = node.children.length;
}
});
}
if (this.config.selection.require && !this.selected().length) {
this.selectFirstAvailableNode();
}
const init = () => {
this.emit('model.loaded', this.model);
resolve(this.model);
this.model.applyChanges();
};
// Delay event for synchronous loader
if (!this.initialized && _.isArray(nodes)) {
setTimeout(init);
}
else {
init();
}
};
// Data given already as an array
if (_.isArrayLike(loader)) {
complete(loader);
}
// Data loader requires a caller/callback
else if (_.isFunction(loader)) {
const resp = loader(null, complete, reject, this.pagination());
// Loader returned its own object
if (resp) {
loader = resp;
}
}
// Data loader is likely a promise
if (_.isObject(loader)) {
standardizePromise(loader).then(complete).catch(reject);
}
else {
reject(new Error('Invalid data loader.'));
}
});
// Copy to event listeners
promise.catch(err => {
this.emit('data.loaderror', err);
});
// Cache to allow access after tree instantiation
this._loader = { promise };
return promise;
}
/**
* Query for all nodes currently loading children.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
loading() {
return map(this, 'loading', arguments);
}
/**
* Load additional nodes for the root context.
*
* @param {Event} event Click or scroll event if DOM interaction triggered this call.
* @return {Promise<TreeNodes>} Resolves with request results.
*/
loadMore() {
return map(this, 'loadMore', arguments);
}
/**
* Create a new collection after passing every node through iteratee.
*
* @param {function} iteratee Node iteratee.
* @return {TreeNodes} New array of node objects.
*/
map() {
return map(this, 'map', arguments);
}
/**
* Query for all nodes matched in the last search.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
matched() {
return map(this, 'matched', arguments);
}
/**
* Move node at a given index to a new index.
*
* @param {int} index Current index.
* @param {int} newIndex New index.
* @param {TreeNodes} target Target TreeNodes array. Defaults to this.
* @return {TreeNode} Node object.
*/
move() {
return map(this, 'move', arguments);
}
/**
* Pause events.
*
* @param {array} events Event names to mute.
* @return {Tree} Tree instance.
*/
mute(events) {
if (_.isString(events) || _.isArray(events)) {
this._muted = _.castArray(events);
}
else {
this._muted = true;
}
return this;
}
/**
* Get current mute settings.
*
* @return {boolean|array} Muted events. If all, true.
*/
muted() {
return this._muted;
}
/**
* Get a node.
*
* @param {string|number} id ID of node.
* @return {TreeNode} Node object.
*/
node() {
return map(this, 'node', arguments);
}
/**
* Get all nodes in a tree, or nodes for an array of IDs.
*
* @param {array} refs Array of ID references.
* @return {TreeNodes} Array of node objects.
* @example
*
* const all = tree.nodes()
* const some = tree.nodes([1, 2, 3])
*/
nodes() {
return map(this, 'nodes', arguments);
}
/**
* Get the root TreeNodes pagination.
*
* @return {object} Pagination configuration object.
*/
pagination() {
return map(this, 'pagination', arguments);
}
/**
* Pop node in the final index position.
*
* @return {TreeNode} Node object.
*/
pop() {
return map(this, 'pop', arguments);
}
/**
* Add a TreeNode to the end of the root collection.
*
* @param {TreeNode} node Node object.
* @return {int} The new length
*/
push() {
return map(this, 'push', arguments);
}
/**
* Iterate down all nodes and any children.
*
* Return false to stop execution.
*
* @private
* @param {function} iteratee Iteratee function
* @return {TreeNodes} Resulting nodes.
*/
recurseDown() {
return map(this, 'recurseDown', arguments);
}
/**
* Reduce nodes.
*
* @param {function} iteratee Iteratee function
* @return {any} Resulting data.
*/
reduce() {
return map(this, 'reduce', arguments);
}
/**
* Right-reduce root nodes.
*
* @param {function} iteratee Iteratee function
* @return {any} Resulting data.
*/
reduceRight() {
return map(this, 'reduceRight', arguments);
}
/**
* Reload/re-execute the original data loader.
*
* @return {Promise<TreeNodes>} Load method promise.
*/
reload() {
this.reset();
return this.load(this.opts.data || this.config.data);
}
/**
* Remove a node.
*
* @param {TreeNode} node Node object.
* @return {TreeNodes} Array of node objects.
*/
remove() {
return map(this, 'remove', arguments);
}
/**
* Remove all nodes.
*
* @return {Tree} Tree instance.
*/
removeAll() {
this.reset().applyChanges();
return this;
}
/**
* Query for all soft-removed nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
removed() {
return map(this, 'removed', arguments);
}
/**
* Resets the root model and associated information like pagination.
*
* Note: This method does *not* apply changes because it assumes
* futher changes will occur to the model.
*
* @private
* @return {Tree} Tree instance.
*/
reset() {
this.model = new TreeNodes(this);
return this;
}
/**
* Restore nodes.
*
* @return {TreeNodes} Array of node objects.
*/
restore() {
return map(this, 'restore', arguments);
}
/**
* Restore (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
restoreDeep() {
return map(this, 'restoreDeep', arguments);
}
/**
* Reverse node order.
*
* @return {TreeNodes} Reversed array of node objects.
*/
reverse() {
return map(this, 'reverse', arguments);
}
/**
* Search nodes, showing only those that match and the necessary hierarchy.
*
* @param {*} query Search string, RegExp, or function.
* @return {Promise<TreeNodes>} Promise resolved with an array of matching node objects.
*/
search(query) {
let { matcher, matchProcessor } = this.config.search;
// Don't search if query empty
if (!query || (_.isString(query) && _.isEmpty(query))) {
return Promise.resolve(this.clearSearch());
}
this.batch();
// Reset states
this.recurseDown(node => {
node.state('hidden', true);
node.state('matched', false);
});
this.end();
// Query nodes for any matching the query
matcher = _.isFunction(matcher) ? matcher : (matchQuery, resolve) => {
const matches = new TreeNodes(this);
// Convery the query into a usable predicate
if (_.isString(matchQuery)) {
matchQuery = new RegExp(matchQuery, 'i');
}
let predicate;
if (_.isRegExp(matchQuery)) {
predicate = node => matchQuery.test(node.text);
}
else {
predicate = matchQuery;
}
// Recurse down and find all matches
this.model.recurseDown(node => {
if (!node.removed()) {
if (predicate(node)) {
// Return as a match
matches.push(node);
}
}
});
resolve(matches);
};
// Process all matching nodes.
matchProcessor = _.isFunction(matchProcessor) ? matchProcessor : matches => {
matches.each(node => {
node.show().state('matched', true);
node.expandParents().collapse();
if (node.hasChildren()) {
node.children.showDeep();
}
});
};
// Wrap the search matcher with a promise since it could require async requests
return new Promise((resolve, reject) => {
// Execute the matcher and pipe results to the processor
matcher(query, matches => {
// Convert to a TreeNodes array if we're receiving external nodes
if (!InspireTree.isTreeNodes(matches)) {
matches = this.nodes(_.map(matches, 'id'));
}
this.batch();
matchProcessor(matches);
this.end();
resolve(matches);
}, reject);
});
}
/**
* Select nodes.
*
* @return {TreeNodes} Array of node objects.
*/
select() {
return map(this, 'select', arguments);
}
/**
* Query for all selectable nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
selectable() {
return map(this, 'selectable', arguments);
}
/**
* Select all nodes between a start and end node.
* Starting node must have a higher index path so we can work down to endNode.
*
* @param {TreeNode} startNode Starting node
* @param {TreeNode} endNode Ending node
* @return {Tree} Tree instance.
*/
selectBetween(startNode, endNode) {
this.batch();
let node = startNode.nextVisibleNode();
while (node.id !== endNode.id) {
node.select();
node = node.nextVisibleNode();
}
this.end();
return this;
}
/**
* Select (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
selectDeep() {
return map(this, 'selectDeep', arguments);
}
/**
* Query for all selected nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
selected() {
return map(this, 'selected', arguments);
}
/**
* Select the first available node.
*
* @return {TreeNode} Selected node object.
*/
selectFirstAvailableNode() {
const node = this.model.filterBy('available').get(0);
if (node) {
node.select();
}
return node;
}
/**
* Shift node in the first index position.
*
* @return {TreeNode} Node object.
*/
shift() {
return map(this, 'shift', arguments);
}
/**
* Show nodes.
*
* @return {TreeNodes} Array of node objects.
*/
show() {
return map(this, 'show', arguments);
}
/**
* Show (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
showDeep() {
return map(this, 'showDeep', arguments);
}
/**
* Get a shallow copy of a portion of nodes.
*
* @param {int} begin Starting index.
* @param {int} end End index.
* @return {Array} Array of selected subset.
*/
slice() {
return map(this, 'slice', arguments);
}
/**
* Soft-remove nodes.
*
* @return {TreeNodes} Array of node objects.
*/
softRemove() {
return map(this, 'softRemove', arguments);
}
/**
* Check if at least one node passes the given test.
*
* @param {function} tester Test each node in this collection,
* @return {boolean} True if at least one node passes the test.
*/
some() {
return map(this, 'some', arguments);
}
/**
* Sort nodes using a function.
*
* @param {function} compareFn Comparison function.
* @return {TreeNodes} Root array of node objects.
*/
sort() {
return map(this, 'sort', arguments);
}
/**
* Sort nodes using a function or key name.
*
* If no custom sorter given, the configured "sort" value will be used.
*
* @param {string|function} sorter Sort function or property name.
* @return {TreeNodes} Array of node obejcts.
*/
sortBy() {
return map(this, 'sortBy', arguments);
}
/**
* Deeply sort nodes.
*
* @param {function} compareFn Comparison function.
* @return {TreeNodes} Root array of node objects.
*/
sortDeep() {
return map(this, 'sortDeep', arguments);
}
/**
* Remove and/or add new TreeNodes into the root collection.
*
* @param {int} start Starting index.
* @param {int} deleteCount Count of nodes to delete.
* @param {TreeNode} node Node(s) to insert.
* @return {Array} Array of selected subset.
*/
splice() {
return map(this, 'slice', arguments);
}
/**
* Set nodes' state values.
*
* @param {string} name Property name.
* @param {boolean} newVal New value, if setting.
* @return {TreeNodes} Array of node objects.
*/
state() {
return map(this, 'state', arguments);
}
/**
* Set (deeply) nodes' state values.
*
* @param {string} name Property name.
* @param {boolean} newVal New value, if setting.
* @return {TreeNodes} Array of node objects.
*/
stateDeep() {
return map(this, 'stateDeep', arguments);
}
/**
* Swap two node positions.
*
* @param {TreeNode} node1 Node 1.
* @param {TreeNode} node2 Node 2.
* @return {TreeNodes} Array of node objects.
*/
swap() {
return map(this, 'swap', arguments);
}
/**
* Get a native node Array.
*
* @return {array} Array of node objects.
*/
toArray() {
return map(this, 'toArray', arguments);
}
/**
* Get a string representation of node objects.
*
* @return {string} Strings from root node objects.
*/
toString() {
return map(this, 'toString', arguments);
}
/**
* Resume events.
*
* @param {array} events Events to unmute.
* @return {Tree} Tree instance.
*/
unmute(events) {
// Diff array and set to false if we're now empty
if (_.isString(events) || _.isArray(events)) {
this._muted = _.difference(this._muted, _.castArray(events));
if (!this._muted.length) {
this._muted = false;
}
}
else {
this._muted = false;
}
return this;
}
/**
* Add a TreeNode in the first index position.
*
* @return {number} The new length
*/
unshift() {
return map(this, 'unshift', arguments);
}
/**
* Query for all visible nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
visible() {
return map(this, 'visible', arguments);
}
}
export default InspireTree;