inspire-tree
Version:
Inspired JavaScript Tree UI Component
1,673 lines (1,550 loc) • 190 kB
JavaScript
/* Inspire Tree
* @version 7.3.0
* https://github.com/helion3/inspire-tree
* @copyright Copyright 2015 Helion3, and other contributors
* @license Licensed under MIT
* see https://github.com/helion3/inspire-tree/blob/master/LICENSE
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('lodash/map.js'), require('lodash/castArray.js'), require('lodash/defaultsDeep.js'), require('lodash/difference.js'), require('lodash/each.js'), require('lodash/get.js'), require('lodash/includes.js'), require('lodash/isArray.js'), require('lodash/isArrayLike.js'), require('lodash/isBoolean.js'), require('lodash/isEmpty.js'), require('lodash/isFunction.js'), require('lodash/isObject.js'), require('lodash/isRegExp.js'), require('lodash/isString.js'), require('lodash/noop.js'), require('lodash/tail.js'), require('lodash/sortBy.js'), require('lodash/assign.js'), require('lodash/cloneDeep.js'), require('lodash/find.js'), require('lodash/findIndex.js'), require('lodash/findLast.js'), require('lodash/indexOf.js'), require('lodash/pull.js'), require('lodash/slice.js'), require('lodash/invoke.js'), require('lodash/isArrayLikeObject.js'), require('lodash/remove.js'), require('lodash/sortedIndexBy.js')) :
typeof define === 'function' && define.amd ? define(['lodash/map.js', 'lodash/castArray.js', 'lodash/defaultsDeep.js', 'lodash/difference.js', 'lodash/each.js', 'lodash/get.js', 'lodash/includes.js', 'lodash/isArray.js', 'lodash/isArrayLike.js', 'lodash/isBoolean.js', 'lodash/isEmpty.js', 'lodash/isFunction.js', 'lodash/isObject.js', 'lodash/isRegExp.js', 'lodash/isString.js', 'lodash/noop.js', 'lodash/tail.js', 'lodash/sortBy.js', 'lodash/assign.js', 'lodash/cloneDeep.js', 'lodash/find.js', 'lodash/findIndex.js', 'lodash/findLast.js', 'lodash/indexOf.js', 'lodash/pull.js', 'lodash/slice.js', 'lodash/invoke.js', 'lodash/isArrayLikeObject.js', 'lodash/remove.js', 'lodash/sortedIndexBy.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.InspireTree = factory(global._.map, global._.castArray, global._.defaultsDeep, global._.difference, global._.each, global._.get, global._.includes, global._.isArray, global._.isArrayLike, global._.isBoolean, global._.isEmpty, global._.isFunction, global._.isObject, global._.isRegExp, global._.isString, global._.noop, global._.tail, global._.sortBy, global._.assign, global._.cloneDeep, global._.find, global._.findIndex, global._.findLast, global._.indexOf, global._.pull, global._.slice, global._.invoke, global._.isArrayLikeObject, global._.remove, global._.sortedIndexBy));
})(this, (function (_map, castArray, defaultsDeep, difference, each, get, includes, isArray, isArrayLike, isBoolean, isEmpty, isFunction, isObject, isRegExp, isString, noop, tail, _sortBy, _assign, cloneDeep, find, findIndex, findLast, indexOf, pull, slice, invoke, isArrayLikeObject, _remove, sortedIndexBy) { 'use strict';
function _assertThisInitialized(e) {
if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
return e;
}
function _callSuper(t, o, e) {
return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e));
}
function _classCallCheck(a, n) {
if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
}
function _defineProperties(e, r) {
for (var t = 0; t < r.length; t++) {
var o = r[t];
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);
}
}
function _createClass(e, r, t) {
return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
writable: !1
}), e;
}
function _get() {
return _get = "undefined" != typeof Reflect && Reflect.get ? Reflect.get.bind() : function (e, t, r) {
var p = _superPropBase(e, t);
if (p) {
var n = Object.getOwnPropertyDescriptor(p, t);
return n.get ? n.get.call(arguments.length < 3 ? e : r) : n.value;
}
}, _get.apply(null, arguments);
}
function _getPrototypeOf(t) {
return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) {
return t.__proto__ || Object.getPrototypeOf(t);
}, _getPrototypeOf(t);
}
function _inherits(t, e) {
if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function");
t.prototype = Object.create(e && e.prototype, {
constructor: {
value: t,
writable: !0,
configurable: !0
}
}), Object.defineProperty(t, "prototype", {
writable: !1
}), e && _setPrototypeOf(t, e);
}
function _isNativeReflectConstruct() {
try {
var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
} catch (t) {}
return (_isNativeReflectConstruct = function () {
return !!t;
})();
}
function _possibleConstructorReturn(t, e) {
if (e && ("object" == typeof e || "function" == typeof e)) return e;
if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined");
return _assertThisInitialized(t);
}
function _setPrototypeOf(t, e) {
return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {
return t.__proto__ = e, t;
}, _setPrototypeOf(t, e);
}
function _superPropBase(t, o) {
for (; !{}.hasOwnProperty.call(t, o) && null !== (t = _getPrototypeOf(t)););
return t;
}
function _superPropGet(t, o, e, r) {
var p = _get(_getPrototypeOf(1 & r ? t.prototype : t), o, e);
return 2 & r && "function" == typeof p ? function (t) {
return p.apply(e, t);
} : p;
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
/**
* Reset a node's state to the tree default.
*
* @private
* @param {TreeNode} node Node object.
* @returns {TreeNode} Node object.
*/
function resetState(node) {
each(node._tree.defaultState, function (val, prop) {
node.state(prop, val);
});
return node;
}
/**
* Stores repetitive state change logic for most state methods.
*
* @private
* @param {string} prop State property name.
* @param {boolean} value New state value.
* @param {string} verb Verb used for events.
* @param {TreeNode} node Node object.
* @param {string} deep Optional name of state method to call recursively.
* @return {TreeNode} Node object.
*/
function baseStateChange(prop, value, verb, node, deep) {
if (node.state(prop) !== value) {
node.context().batch();
if (node._tree.config.nodes.resetStateOnRestore && verb === 'restored') {
resetState(node);
}
// indeterminate may never be true if checked is
if (value && prop === 'checked') {
node.state('indeterminate', false);
}
node.state(prop, value);
node._tree.emit('node.' + verb, node, false);
if (deep && node.hasChildren()) {
node.children.recurseDown(function (child) {
baseStateChange(prop, value, verb, child);
});
}
// This node's "renderability" has changed, so we should
// trigger a re-cache in the parent context.
if (prop === 'hidden' || prop === 'removed') {
node.context().indicesDirty = true;
node.context().calculateRenderablePositions();
}
node.markDirty();
node.context().end();
}
return node;
}
function _extendableBuiltin(cls) {
function ExtendableBuiltin() {
cls.apply(this, arguments);
}
ExtendableBuiltin.prototype = Object.create(cls.prototype, {
constructor: {
value: cls,
enumerable: false,
writable: true,
configurable: true
}
});
if (Object.setPrototypeOf) {
Object.setPrototypeOf(ExtendableBuiltin, cls);
} else {
ExtendableBuiltin.__proto__ = cls;
}
return ExtendableBuiltin;
}
/**
* Base function to invoke given method(s) on tree nodes.
*
* @private
* @param {TreeNode} nodes Array of node objects.
* @param {string|array} methods Method names.
* @param {array|Arguments} args Array of arguments to proxy.
* @param {boolean} deep Invoke deeply.
* @return {TreeNodes} Array of node objects.
*/
function baseInvoke(nodes, methods, args, deep) {
methods = castArray(methods);
nodes._tree.batch();
nodes[deep ? 'recurseDown' : 'each'](function (node) {
each(methods, function (method) {
if (isFunction(node[method])) {
node[method].apply(node, args);
}
});
});
nodes._tree.end();
return nodes;
}
/**
* Creates a predicate function.
*
* @private
* @param {string|function} predicate Property name or custom function.
* @return {function} Predicate function.
*/
function getPredicateFunction(predicate) {
var fn = predicate;
if (isString(predicate)) {
fn = function fn(node) {
return isFunction(node[predicate]) ? node[predicate]() : node[predicate];
};
}
return fn;
}
/**
* Base function to filter nodes by state value.
*
* @private
* @param {string} state State property
* @param {boolean} full Return a non-flat hierarchy
* @return {TreeNodes} Array of matching nodes.
*/
function baseStatePredicate(state, full) {
if (full) {
return this.extract(state);
}
// Cache a state predicate function
var fn = getPredicateFunction(state);
return this.flatten(function (node) {
// Never include removed nodes unless specifically requested
if (state !== 'removed' && node.removed()) {
return false;
}
return fn(node);
});
}
/**
* An Array-like collection of TreeNodes.
*
* Note: Due to issue in many javascript environments,
* native objects are problematic to extend correctly
* so we mimic it, not actually extend it.
*
* @param {InspireTree} tree Context tree.
* @param {array} array Array of TreeNode objects.
* @param {object} opts Configuration object.
* @return {TreeNodes} Collection of TreeNode
*/
var TreeNodes = /*#__PURE__*/function (_extendableBuiltin2) {
function TreeNodes(tree, array, opts) {
var _this;
_classCallCheck(this, TreeNodes);
_this = _callSuper(this, TreeNodes);
if (isFunction(get(tree, 'isTree')) && !tree.isTree(tree)) {
throw new TypeError('Invalid tree instance.');
}
Object.defineProperty(_this, '_tree', {
value: tree,
writable: true
});
_this.length = 0;
_this.batching = 0;
// A custom dirty flag to indicate when an index-altering
// change has occured. Avoids re-caching when unnecessary.
_this.indicesDirty = false;
_this.config = defaultsDeep({}, opts, {
calculateRenderablePositions: false
});
// Init pagination
_this._pagination = {
limit: tree.config.pagination.limit,
total: 0
};
if (isArray(array) || array instanceof TreeNodes) {
each(array, function (node) {
if (node instanceof TreeNode) {
_this.push(node.clone());
} else {
_this.addNode(node);
}
});
}
return _this;
}
/**
* Adds a new node. If a sort method is configured,
* the node will be added in the appropriate order.
*
* @param {object} object Node
* @return {TreeNode} Node object.
*/
_inherits(TreeNodes, _extendableBuiltin2);
return _createClass(TreeNodes, [{
key: "addNode",
value: function addNode(object) {
// Base insertion index
var index = this.length;
// If tree is sorted, insert in correct position
if (this._tree.config.sort) {
index = sortedIndexBy(this, object, this._tree.config.sort);
}
return this.insertAt(index, object);
}
/**
* 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}
*/
}, {
key: "applyChanges",
value: function applyChanges() {
if (this.batching === 0) {
this.calculateRenderablePositions();
this._tree.emit('changes.applied', this.context());
}
}
/**
* Batch multiple changes for listeners (i.e. DOM)
*
* @private
* @return {void}
*/
}, {
key: "batch",
value: function batch() {
if (this.batching < 0) {
this.batching = 0;
}
this.batching++;
}
/**
* Query for all available nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "available",
value: function available(full) {
return baseStatePredicate.call(this, 'available', full);
}
/**
* Blur nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "blur",
value: function blur() {
return this.invoke('blur');
}
/**
* Blur (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "blurDeep",
value: function blurDeep() {
return this.invokeDeep('blur');
}
/**
* Calculate and cache the first/last renderable nodes.
*
* Primarily useful for rendering engines, since hidden DOM
* nodes may still be present and CSS :first/:last selectors
* would fail.
*
* @param {boolean} force Force recalculation.
* @return {void}
* @private
*/
}, {
key: "calculateRenderablePositions",
value: function calculateRenderablePositions(force) {
if (!force && (!this.indicesDirty || this.batching > 0 || !this.config.calculateRenderablePositions)) {
return;
}
var first;
var last;
this.each(function (node) {
if (node.renderable()) {
// Cache first node if none yet
first = first || node;
// Always update last node on match
last = node;
}
});
if (this.firstRenderableNode && this.firstRenderableNode !== first) {
this.firstRenderableNode.markDirty();
}
if (first && first !== this.firstRenderableNode) {
first.markDirty();
}
if (this.lastRenderableNode && this.lastRenderableNode !== last) {
this.lastRenderableNode.markDirty();
}
if (last && last !== this.lastRenderableNode) {
last.markDirty();
}
this.firstRenderableNode = first;
this.lastRenderableNode = last;
this.indicesDirty = false;
// If we have a parent, force it to recalculate as well
if (this._context && this._context.context() instanceof TreeNodes) {
this._context.context().calculateRenderablePositions(true);
}
}
/**
* Query for all checked nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "checked",
value: function checked(full) {
return baseStatePredicate.call(this, 'checked', full);
}
/**
* Clean nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "clean",
value: function clean() {
return this.invoke('clean');
}
/**
* Clones (deeply) the array of nodes.
*
* Note: Cloning will *not* clone the context pointer.
*
* @return {TreeNodes} Array of cloned nodes.
*/
}, {
key: "clone",
value: function clone() {
return new TreeNodes(this._tree, this);
}
/**
* Collapse nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "collapse",
value: function collapse() {
return this.invoke('collapse');
}
/**
* Query for all collapsed nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "collapsed",
value: function collapsed(full) {
return baseStatePredicate.call(this, 'collapsed', full);
}
/**
* Collapse (deeply) all children.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "collapseDeep",
value: function collapseDeep() {
return this.invokeDeep('collapse');
}
/**
* Concat multiple TreeNodes arrays.
*
* @param {TreeNodes} nodes Array of nodes.
* @return {TreeNodes} Resulting node array.
*/
}, {
key: "concat",
value: function concat(nodes) {
var newNodes = new TreeNodes(this._tree);
newNodes._context = this._context;
var pusher = function pusher(node) {
if (node instanceof TreeNode) {
newNodes.push(node);
}
};
each(this, pusher);
each(nodes, pusher);
// Copy pagination limit
newNodes._pagination.limit = this._pagination.limit;
return newNodes;
}
/**
* Get the context of this collection. If a collection
* of children, context is the parent node. Otherwise
* the context is the tree itself.
*
* @return {TreeNode|object} Node object or tree instance.
*/
}, {
key: "context",
value: function context() {
return this._context || this._tree;
}
/**
* 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.
*/
}, {
key: "copy",
value: function copy(dest, hierarchy, includeState) {
var newNodes = new TreeNodes(this._tree);
each(this, function (node) {
newNodes.push(node.copy(dest, hierarchy, includeState));
});
return newNodes;
}
/**
* Return deepest nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "deepest",
value: function deepest() {
var matches = new TreeNodes(this._tree);
this.recurseDown(function (node) {
if (!node.children) {
matches.push(node);
}
});
return matches;
}
/**
* Deselect nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "deselect",
value: function deselect() {
return this.invoke('deselect');
}
/**
* Deselect (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "deselectDeep",
value: function deselectDeep() {
return this.invokeDeep('deselect');
}
/**
* Iterate each TreeNode.
*
* @param {function} iteratee Iteratee invoke for each node.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "each",
value: function each$1(iteratee) {
each(this, iteratee);
return this;
}
/**
* Query for all editable nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "editable",
value: function editable(full) {
return baseStatePredicate.call(this, 'editable', full);
}
/**
* Query for all nodes in editing mode.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "editing",
value: function editing(full) {
return baseStatePredicate.call(this, 'editing', full);
}
/**
* Release the current batch.
*
* @private
* @return {void}
*/
}, {
key: "end",
value: function end() {
this.batching--;
if (this.batching === 0) {
this.applyChanges();
}
}
/**
* Expand nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "expand",
value: function expand() {
return this.invoke('expand');
}
/**
* Query for all expanded nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "expanded",
value: function expanded(full) {
return baseStatePredicate.call(this, 'expanded', full);
}
/**
* Expand (deeply) all nodes.
*
* @return {Promise<TreeNodes>} Promise resolved when all children have loaded and expanded.
*/
}, {
key: "expandDeep",
value: function expandDeep() {
var _this2 = this;
return new Promise(function (resolve) {
var waitCount = 0;
var done = function done() {
if (--waitCount === 0) {
resolve(_this2);
}
};
_this2.recurseDown(function (node) {
waitCount++;
// Ignore nodes without children
if (node.children) {
node.expand()["catch"](done).then(function () {
// Manually trigger expansion on newly loaded children
node.children.expandDeep()["catch"](done).then(done);
});
} else {
done();
}
});
});
}
/**
* Expand parents.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "expandParents",
value: function expandParents() {
return this.invoke('expandParents');
}
/**
* 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.
*/
}, {
key: "extract",
value: function extract(predicate) {
var flat = this.flatten(predicate);
var matches = new TreeNodes(this._tree);
each(flat, function (node) {
return matches.addNode(node.copyHierarchy());
});
return matches;
}
/**
* Filter all nodes matching the given predicate.
*
* @param {string|function} predicate State flag or custom function.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "filterBy",
value: function filterBy(predicate) {
var fn = getPredicateFunction(predicate);
var matches = new TreeNodes(this._tree);
each(this, function (node) {
if (fn(node)) {
matches.push(node);
}
});
return matches;
}
/**
* 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.
*/
}, {
key: "find",
value: function find(predicate) {
var match;
this.recurseDown(function (node) {
if (predicate(node)) {
match = node;
return false;
}
});
return match;
}
/**
* 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.
*/
}, {
key: "first",
value: function first(predicate) {
for (var i = 0, l = this.length; i < l; i++) {
if (predicate(this[i])) {
return this[i];
}
}
}
/**
* 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.
*/
}, {
key: "flatten",
value: function flatten(predicate) {
var flat = new TreeNodes(this._tree);
var fn = getPredicateFunction(predicate);
this.recurseDown(function (node) {
if (fn(node)) {
flat.push(node);
}
});
return flat;
}
/**
* Query for all focused nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "focused",
value: function focused(full) {
return baseStatePredicate.call(this, 'focused', full);
}
/**
* Iterate each TreeNode.
*
* @param {function} iteratee Iteratee invoke for each node.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "forEach",
value: function forEach(iteratee) {
return this.each(iteratee);
}
/**
* 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.
*/
}, {
key: "get",
value: function get(index) {
return this[index];
}
/**
* Query for all hidden nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "hidden",
value: function hidden(full) {
return baseStatePredicate.call(this, 'hidden', full);
}
/**
* Hide nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "hide",
value: function hide() {
return this.invoke('hide');
}
/**
* Hide (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "hideDeep",
value: function hideDeep() {
return this.invokeDeep('hide');
}
/**
* Query for all indeterminate nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "indeterminate",
value: function indeterminate(full) {
return baseStatePredicate.call(this, 'indeterminate', full);
}
/**
* Insert a new node at a given position.
*
* @param {integer} index Index at which to insert the node.
* @param {object} object Raw node object or TreeNode.
* @return {TreeNode} Node object.
*/
}, {
key: "insertAt",
value: function insertAt(index, object) {
// If node has a pre-existing ID
if (object.id) {
// Is it already in the tree?
var existingNode = this.node(object.id);
if (existingNode) {
existingNode.restore().show();
// Merge children
if (isArrayLike(object.children)) {
// Setup existing node's children property if needed
if (!isArrayLike(existingNode.children)) {
existingNode.children = new TreeNodes(this._tree);
existingNode.children._context = existingNode;
}
// Copy each child (using addNode, which uses insertAt)
each(object.children, function (child) {
existingNode.children.addNode(child);
});
}
// Merge truthy children
else if (object.children && isBoolean(existingNode.children)) {
existingNode.children = object.children;
}
existingNode.markDirty();
this.applyChanges();
// Node merged, return it.
return existingNode;
}
}
// Node is new, insert at given location.
var node = this._tree.constructor.isTreeNode(object) ? object : objectToNode(this._tree, object);
// Grab remaining nodes
this.splice(index, 0, node);
// Refresh parent state and mark dirty
if (this._context) {
node.itree.parent = this._context;
this._context.refreshIndeterminateState().markDirty();
}
// Event
this._tree.emit('node.added', node);
// Always mark this node as dirty
node.markDirty();
// If pushing this node anywhere but the end, other nodes may change.
if (this.length - 1 !== index) {
this.invoke('markDirty');
}
this.applyChanges();
return node;
}
/**
* Invoke method(s) on each node.
*
* @param {string|array} methods Method name(s).
* @param {array|Arguments} args Array of arguments to proxy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "invoke",
value: function invoke(methods, args) {
return baseInvoke(this, methods, args);
}
/**
* Invoke method(s) deeply.
*
* @param {string|array} methods Method name(s).
* @param {array|Arguments} args Array of arguments to proxy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "invokeDeep",
value: function invokeDeep(methods, args) {
if (!isArrayLikeObject(args) || arguments.length > 2) {
args = tail(arguments);
}
return baseInvoke(this, methods, args, true);
}
/**
* 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.
*/
}, {
key: "last",
value: function last(predicate) {
for (var i = this.length - 1; i >= 0; i--) {
if (predicate(this[i])) {
return this[i];
}
}
}
/**
* Query for all nodes currently loading children.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "loading",
value: function loading(full) {
return baseStatePredicate.call(this, 'loading', full);
}
/**
* Loads additional nodes for this context.
*
* @param {Event} event Click or scroll event if DOM interaction triggered this call.
* @return {Promise<TreeNodes>} Resolves with request results.
*/
}, {
key: "loadMore",
value: function loadMore(event) {
var _this3 = this;
// Never refire if node is loading
if (this._loading) {
return Promise.reject(new Error('Pending loadMore call must complete before being invoked again.'));
}
var promise;
// If no records remain, immediately resolve
if (this._pagination.limit === this._pagination.total) {
return Promise.resolve();
}
// Set loading flag, prevents repeat requests
this._loading = true;
this.batch();
// Mark this context as dirty since we'll update text/tree nodes
invoke(this._context, 'markDirty');
// Increment the pagination
this._pagination.limit += this._tree.config.pagination.limit;
// Emit an event
this._tree.emit('node.paginated', this._context || this._tree, this.pagination, event);
if (this._tree.config.deferredLoading) {
if (this._context) {
promise = this._context.loadChildren();
} else {
promise = this._tree.load(this._tree.config.data);
}
} else {
this._loading = false;
promise = Promise.resolve();
}
this.end();
// Clear the loading flag
if (this._tree.config.deferredLoading) {
promise.then(function () {
_this3._loading = false;
_this3.applyChanges();
})["catch"](function () {
_this3._loading = false;
_this3.applyChanges();
});
}
return promise;
}
/**
* Query for all nodes matched in the last search.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "matched",
value: function matched(full) {
return baseStatePredicate.call(this, 'matched', full);
}
/**
* 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.
*/
}, {
key: "move",
value: function move(index, newIndex) {
var target = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this;
var oldNode = this[index].remove();
var node = target.insertAt(newIndex, oldNode);
this._tree.emit('node.moved', node, this, index, target, newIndex);
return node;
}
/**
* Get a node.
*
* @param {string|number} id ID of node.
* @return {TreeNode} Node object.
*/
}, {
key: "node",
value: function node(id) {
var match;
this.recurseDown(function (node) {
if (node.id === id) {
match = node;
return false;
}
});
return match;
}
/**
* 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])
*/
}, {
key: "nodes",
value: function nodes(refs) {
var results;
if (isArray(refs)) {
results = new TreeNodes(this._tree);
this.recurseDown(function (node) {
if (refs.indexOf(node.id) > -1) {
results.push(node);
}
});
}
return isArray(refs) ? results : this;
}
/**
* Get the pagination.
*
* @return {object} Pagination configuration object.
*/
}, {
key: "pagination",
value: function pagination() {
return this._pagination;
}
/**
* Removes the last node.
*
* @return {TreeNode} Last tree node.
*/
}, {
key: "pop",
value: function pop() {
var result = _superPropGet(TreeNodes, "pop", this, 3)([]);
this.indicesDirty = true;
this.calculateRenderablePositions();
return result;
}
/**
* Push a new TreeNode onto the collection.
*
* @param {TreeNode} node Node objext.
* @returns {number} The new length.
*/
}, {
key: "push",
value: function push(node) {
var result = _superPropGet(TreeNodes, "push", this, 3)([node]);
this.indicesDirty = true;
this.calculateRenderablePositions();
return result;
}
/**
* Iterate down all nodes and any children.
*
* Return false to stop execution.
*
* @param {function} iteratee Iteratee function.
* @return {TreeNodes} Resulting nodes.
*/
}, {
key: "recurseDown",
value: function recurseDown$1(iteratee) {
recurseDown(this, iteratee);
return this;
}
/**
* Remove a node.
*
* @param {TreeNode} node Node object.
* @return {TreeNodes} Resulting nodes.
*/
}, {
key: "remove",
value: function remove(node) {
_remove(this, {
id: node.id
});
invoke(this._context, 'markDirty');
this.indicesDirty = true;
this.applyChanges();
return this;
}
/**
* Query for all soft-removed nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "removed",
value: function removed(full) {
return baseStatePredicate.call(this, 'removed', full);
}
/**
* Restore nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "restore",
value: function restore() {
return this.invoke('restore');
}
/**
* Restore (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "restoreDeep",
value: function restoreDeep() {
return this.invokeDeep('restore');
}
/**
* Select nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "select",
value: function select() {
return this.invoke('select');
}
/**
* Query for all selectable nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "selectable",
value: function selectable(full) {
return baseStatePredicate.call(this, 'selectable', full);
}
/**
* Select (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "selectDeep",
value: function selectDeep() {
return this.invokeDeep('select');
}
/**
* Query for all selected nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "selected",
value: function selected(full) {
return baseStatePredicate.call(this, 'selected', full);
}
/**
* Removes the first node.
*
* @param {TreeNode} node Node object.
* @return {TreeNode} Node object.
*/
}, {
key: "shift",
value: function shift(node) {
var result = _superPropGet(TreeNodes, "shift", this, 3)([node]);
this.indicesDirty = true;
this.calculateRenderablePositions();
return result;
}
/**
* Show nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "show",
value: function show() {
return this.invoke('show');
}
/**
* Show (deeply) all nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "showDeep",
value: function showDeep() {
return this.invokeDeep('show');
}
/**
* Soft-remove nodes.
*
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "softRemove",
value: function softRemove() {
return this.invoke('softRemove');
}
/**
* Sorts all TreeNode objects in this collection.
*
* 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 objects.
*/
}, {
key: "sortBy",
value: function sortBy(sorter) {
var _this4 = this;
sorter = sorter || this._tree.config.sort;
// Only apply sort if one provided
if (sorter) {
this.batch();
var sorted = _sortBy(this, sorter);
this.length = 0;
each(sorted, function (node) {
_this4.push(node);
});
this.indicesDirty = true;
this.end();
}
return this;
}
/**
* Sorts (deeply) all nodes in this collection.
*
* @param {function} comparator [description]
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "sortDeep",
value: function sortDeep(comparator) {
this.sort(comparator);
this.each(function (node) {
if (node.hasChildren()) {
node.children.sortDeep(comparator);
}
});
return this;
}
/**
* Changes array contents by removing existing nodes and/or adding new nodes.
*
* @param {number} start Start index.
* @param {number} deleteCount Number of nodes to delete.
* @param {TreeNode} ...nodes One or more nodes.
* @return {array} Array of deleted elements.
*/
}, {
key: "splice",
value: function splice() {
var result = _superPropGet(TreeNodes, "splice", this, 1).apply(this, arguments);
this.indicesDirty = true;
this.calculateRenderablePositions();
return result;
}
/**
* Set nodes' state values.
*
* @param {string} name Property name.
* @param {boolean} newVal New value, if setting.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "state",
value: function state() {
return this.invoke('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.
*/
}, {
key: "stateDeep",
value: function stateDeep() {
return this.invokeDeep('state', arguments);
}
/**
* Swaps two node positions.
*
* @param {TreeNode} node1 Node 1.
* @param {TreeNode} node2 Node 2.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "swap",
value: function swap(node1, node2) {
this._tree.batch();
var n1Context = node1.context();
var n2Context = node2.context();
// Cache. Note: n2Index is only usable once
var n1Index = n1Context.indexOf(node1);
var n2Index = n2Context.indexOf(node2);
// If contexts match, we can simply re-assign them
if (n1Context === n2Context) {
this[n1Index] = node2;
this[n2Index] = node1;
// Emit move events for each node
this._tree.emit('node.moved', node1, n1Context, n1Index, n2Context, n2Index);
this._tree.emit('node.moved', node2, n2Context, n2Index, n1Context, n1Index);
} else {
// Otherwise, we have to move between contexts
// Move node 1 to node 2's index
n1Context.move(n1Index, n2Context.indexOf(node2), n2Context);
// Move node 2 to node 1s original index
n2Context.move(n2Context.indexOf(node2), n1Index, n1Context);
}
this.indicesDirty = true;
this._tree.end();
this._tree.emit('node.swapped', node1, n1Context, n1Index, node2, n2Context, n2Index);
return this;
}
/**
* Get the tree instance.
*
* @return {InspireTree} Tree instance.
*/
}, {
key: "tree",
value: function tree() {
return this._tree;
}
/**
* Get a native node Array.
*
* @return {array} Array of node objects.
*/
}, {
key: "toArray",
value: function toArray() {
var array = [];
each(this, function (node) {
array.push(node.toObject());
});
return array;
}
/**
* Adds a node to beginning of the collection.
*
* @param {TreeNode} node Node object.
* @return {number} New length of collection.
*/
}, {
key: "unshift",
value: function unshift(node) {
var result = _superPropGet(TreeNodes, "unshift", this, 3)([node]);
this.indicesDirty = true;
this.calculateRenderablePositions();
return result;
}
/**
* Query for all visible nodes.
*
* @param {boolean} full Retain full hiearchy.
* @return {TreeNodes} Array of node objects.
*/
}, {
key: "visible",
value: function visible(full) {
return baseStatePredicate.call(this, 'visible', full);
}
}]);
}(_extendableBuiltin(Array));
/**
* Base recursion function for a collection or node.
*
* Returns false if execution should cease.
*
* @private
* @param {TreeNode|TreeNodes} obj Node or collection.
* @param {function} iteratee Iteratee function
* @return {boolean} Cease iteration.
*/
function recurseDown(obj, iteratee) {
var res;
if (obj instanceof TreeNodes) {
each(obj, function (node) {
res = recurseDown(node, iteratee);
return res;
});
} else if (obj instanceof TreeNode) {
res = iteratee(obj);
// Recurse children
if (res !== false && obj.hasChildren()) {
res = recurseDown(obj.children, iteratee);
}
}
return res;
}
/**
* Resolve promise-like objects consistently.
*
* @private
* @param {object} promise Promise-like object.
* @returns {Promise} Promise
*/
function standardizePromise(promise) {
return new Promise(function (resolve, reject) {
if (!isObject(promise)) {
return reject(new Error('Invalid Promise'));
}
if (isFunction(promise.then)) {
promise.then(resolve);
}
// jQuery promises use "error"
if (isFunction(promise.error)) {
promise.error(reject);
} else if (isFunction(promise["catch"])) {
promise["catch"](reject);
}
});
}
/**
* Helper method to clone an ITree config object.
*
* Rejects non-clonable properties like ref.
*
* @private
* @param {object} itree ITree configuration object
* @param {array} excludeKeys Keys to exclude, if any
* @return {object} Cloned ITree.
*/
function cloneItree(itree, excludeKeys) {
var clone = {};
excludeKeys = castArray(excludeKeys);
excludeKeys.push('ref');
each(itree, function (v, k) {
if (!includes(excludeKeys, k)) {
clone[k] = cloneDeep(v);
}
});
if (!excludeKeys.includes('parent')) {
Object.defineProperty(clone, 'parent', {
value: itree.parent,
writable: true
});
}
return clone;
}
/**
* Get or set a state value.
*
* This is a base method and will not invoke related changes, for example
* setting selected=false will not trigger any deselection logic.
*
* @private
* @param {TreeNode} node Tree node.
* @param {string} property Property name.
* @param {boolean} val New value, if setting.
* @return {boolean} Current value on read, old value on set.
*/
function baseState(node, property, val) {
var currentVal = node.itree.state[property];
if (typeof val !== 'undefined' && currentVal !== val) {
// Update values
node.itree.state[property] = val;
if (property !== 'rendered') {
node.markDirty();
}
// Emit an event
node._tree.emit('node.state.changed', node, property, currentVal, val);
}
return currentVal;
}
/**
* Represents a singe node object within the tree.
*
* @param {TreeNode} source TreeNode to copy.
* @return {TreeNode} Tree node object.
*/
var TreeNode = /*#__PURE__*/function () {
function TreeNode(tree, source, excludeKeys) {
var _this = this;
_classCallCheck(this, TreeNode);
Object.defineProperty(this, '_tree', {
value: tree,
writable: true
});
if (source instanceof TreeNode) {
excludeKeys = castArray(excludeKeys);
excludeKeys.push('_tree');
// Iterate manually for better perf
each(source, function (value, key) {
// Skip properties
if (!includes(excludeKeys, key)) {
if (isObject(value)) {
if (value instanceof TreeNodes) {
_this[key] = value.clone();
} else if (key === 'itree') {
_this[key] = cloneItree(value);
} else {
_this[key] = cloneDeep(value);
}
} else {
// Copy pr