xl-infinite-tree
Version:
A browser-ready tree library that can efficiently display a large amount of data using infinite scrolling.
1,251 lines (1,008 loc) • 75.1 kB
JavaScript
'use strict';
exports.__esModule = true;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _events = require('events');
var _events2 = _interopRequireDefault(_events);
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _elementClass = require('element-class');
var _elementClass2 = _interopRequireDefault(_elementClass);
var _isDom = require('is-dom');
var _isDom2 = _interopRequireDefault(_isDom);
var _flattree = require('flattree');
var _clusterize = require('./clusterize');
var _clusterize2 = _interopRequireDefault(_clusterize);
var _ensureArray = require('./ensure-array');
var _ensureArray2 = _interopRequireDefault(_ensureArray);
var _extend = require('./extend');
var _extend2 = _interopRequireDefault(_extend);
var _utilities = require('./utilities');
var _lookupTable = require('./lookup-table');
var _lookupTable2 = _interopRequireDefault(_lookupTable);
var _renderer = require('./renderer');
var _dom = require('./dom');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* eslint no-continue: 0 */
/* eslint operator-assignment: 0 */
var noop = function noop() {};
var error = function error(format) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
var argIndex = 0;
var message = 'Error: ' + format.replace(/%s/g, function () {
return args[argIndex++];
});
if (console && console.error) {
console.error(message);
}
try {
// This error was thrown as a convenience so that you can use this stack
// to find the callsite that caused this error to fire.
throw new Error(message);
} catch (e) {
// Ignore
}
};
var ensureNodeInstance = function ensureNodeInstance(node) {
if (!node) {
// undefined or null
return false;
}
if (!(node instanceof _flattree.Node)) {
error('The node must be a Node object.');
return false;
}
return true;
};
var createRootNode = function createRootNode(rootNode) {
return (0, _extend2['default'])(rootNode || new _flattree.Node(), {
parent: null,
children: [],
state: {
depth: -1,
open: true, // always open
path: '',
prefixMask: '',
total: 0
}
});
};
var InfiniteTree = function (_events$EventEmitter) {
_inherits(InfiniteTree, _events$EventEmitter);
// Creates new InfiniteTree object.
function InfiniteTree(el, options) {
_classCallCheck(this, InfiniteTree);
var _this = _possibleConstructorReturn(this, _events$EventEmitter.call(this));
_this.options = {
autoOpen: false,
blocksInCluster: 4,
droppable: false,
shouldLoadNodes: null,
loadNodes: null,
rowRenderer: _renderer.defaultRowRenderer,
selectable: true,
shouldSelectNode: null,
// When el is not specified, the tree will run in the stealth mode
el: null,
// The following options will have no effect in the stealth mode
layout: 'div',
noDataClass: 'infinite-tree-no-data',
noDataText: 'No data',
nodeIdAttr: 'data-id',
togglerClass: 'infinite-tree-toggler'
};
_this.state = {
openNodes: [],
rootNode: createRootNode(),
selectedNode: null
};
_this.clusterize = null;
_this.nodeTable = new _lookupTable2['default']();
_this.nodes = [];
_this.rows = [];
_this.filtered = false;
_this.scrollElement = null;
_this.contentElement = null;
_this.draggableTarget = null;
_this.droppableTarget = null;
_this.contentListener = {
'click': function click(event) {
event = event || window.event;
// Wrap stopPropagation that allows click event handler to stop execution
// by setting the cancelBubble property
var stopPropagation = event.stopPropagation;
event.stopPropagation = function () {
// Setting the cancelBubble property in browsers that don't support it doesn't hurt.
// Of course it doesn't actually cancel the bubbling, but the assignment itself is safe.
event.cancelBubble = true;
if (stopPropagation) {
stopPropagation.call(event);
}
};
// Call setTimeout(fn, 0) to re-queues the execution of subsequent calls, it allows the
// click event to bubble up to higher level event handlers before handling tree events.
setTimeout(function () {
// Stop execution if the cancelBubble property is set to true by higher level event handlers
if (event.cancelBubble === true) {
return;
}
// Emit a "click" event
_this.emit('click', event);
// Stop execution if the cancelBubble property is set to true after emitting the click event
if (event.cancelBubble === true) {
return;
}
var itemTarget = null;
var clickToggler = false;
if (event.target) {
itemTarget = event.target !== event.currentTarget ? event.target : null;
} else if (event.srcElement) {
// IE8
itemTarget = event.srcElement;
}
while (itemTarget && itemTarget.parentElement !== _this.contentElement) {
if ((0, _elementClass2['default'])(itemTarget).has(_this.options.togglerClass)) {
clickToggler = true;
}
itemTarget = itemTarget.parentElement;
}
if (!itemTarget || itemTarget.hasAttribute('disabled')) {
return;
}
var id = itemTarget.getAttribute(_this.options.nodeIdAttr);
var node = _this.getNodeById(id);
if (!node) {
return;
}
// Click on the toggler to open/close a tree node
if (clickToggler) {
_this.toggleNode(node, { async: true });
return;
}
_this.selectNode(node); // selectNode will re-render the tree
}, 0);
},
'dblclick': function dblclick(event) {
// Emit a "doubleClick" event
_this.emit('doubleClick', event);
},
'keydown': function keydown(event) {
// Emit a "keyDown" event
_this.emit('keyDown', event);
},
'keyup': function keyup(event) {
// Emit a "keyUp" event
_this.emit('keyUp', event);
},
// https://developer.mozilla.org/en-US/docs/Web/Events/dragstart
// The dragstart event is fired when the user starts dragging an element or text selection.
'dragstart': function dragstart(event) {
event = event || window.event;
_this.draggableTarget = event.target || event.srcElement;
},
// https://developer.mozilla.org/en-US/docs/Web/Events/dragend
// The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key).
'dragend': function dragend(event) {
event = event || window.event;
var _this$options$droppab = _this.options.droppable.hoverClass,
hoverClass = _this$options$droppab === undefined ? '' : _this$options$droppab;
// Draggable
_this.draggableTarget = null;
// Droppable
if (_this.droppableTarget) {
(0, _elementClass2['default'])(_this.droppableTarget).remove(hoverClass);
_this.droppableTarget = null;
}
},
// https://developer.mozilla.org/en-US/docs/Web/Events/dragenter
// The dragenter event is fired when a dragged element or text selection enters a valid drop target.
'dragenter': function dragenter(event) {
event = event || window.event;
var itemTarget = null;
if (event.target) {
itemTarget = event.target !== event.currentTarget ? event.target : null;
} else if (event.srcElement) {
// IE8
itemTarget = event.srcElement;
}
while (itemTarget && itemTarget.parentElement !== _this.contentElement) {
itemTarget = itemTarget.parentElement;
}
if (!itemTarget) {
return;
}
if (_this.droppableTarget === itemTarget) {
return;
}
var _this$options$droppab2 = _this.options.droppable,
accept = _this$options$droppab2.accept,
_this$options$droppab3 = _this$options$droppab2.hoverClass,
hoverClass = _this$options$droppab3 === undefined ? '' : _this$options$droppab3;
(0, _elementClass2['default'])(_this.droppableTarget).remove(hoverClass);
_this.droppableTarget = null;
var canDrop = true; // Defaults to true
if (typeof accept === 'function') {
var id = itemTarget.getAttribute(_this.options.nodeIdAttr);
var node = _this.getNodeById(id);
canDrop = !!accept.call(_this, event, {
type: 'dragenter',
draggableTarget: _this.draggableTarget,
droppableTarget: itemTarget,
node: node
});
}
if (canDrop) {
(0, _elementClass2['default'])(itemTarget).add(hoverClass);
_this.droppableTarget = itemTarget;
}
},
// https://developer.mozilla.org/en-US/docs/Web/Events/dragover
// The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds).
'dragover': function dragover(event) {
event = event || window.event;
(0, _dom.preventDefault)(event);
},
// https://developer.mozilla.org/en-US/docs/Web/Events/drop
// The drop event is fired when an element or text selection is dropped on a valid drop target.
'drop': function drop(event) {
event = event || window.event;
// prevent default action (open as link for some elements)
(0, _dom.preventDefault)(event);
if (!(_this.draggableTarget && _this.droppableTarget)) {
return;
}
var _this$options$droppab4 = _this.options.droppable,
accept = _this$options$droppab4.accept,
drop = _this$options$droppab4.drop,
_this$options$droppab5 = _this$options$droppab4.hoverClass,
hoverClass = _this$options$droppab5 === undefined ? '' : _this$options$droppab5;
var id = _this.droppableTarget.getAttribute(_this.options.nodeIdAttr);
var node = _this.getNodeById(id);
var canDrop = true; // Defaults to true
if (typeof accept === 'function') {
canDrop = !!accept.call(_this, event, {
type: 'drop',
draggableTarget: _this.draggableTarget,
droppableTarget: _this.droppableTarget,
node: node
});
}
if (canDrop && typeof drop === 'function') {
drop.call(_this, event, {
draggableTarget: _this.draggableTarget,
droppableTarget: _this.droppableTarget,
node: node
});
}
(0, _elementClass2['default'])(_this.droppableTarget).remove(hoverClass);
_this.droppableTarget = null;
}
};
if ((0, _isDom2['default'])(el)) {
options = _extends({}, options, { el: el });
} else if (el && (typeof el === 'undefined' ? 'undefined' : _typeof(el)) === 'object') {
options = el;
}
// Assign options
_this.options = _extends({}, _this.options, options);
_this.create();
// Load tree data if it's provided
if (_this.options.data) {
_this.loadData(_this.options.data);
}
return _this;
}
// The following elements will have no effect in the stealth mode
InfiniteTree.prototype.create = function create() {
var _this2 = this;
if (this.options.el) {
var tag = null;
this.scrollElement = document.createElement('div');
if (this.options.layout === 'table') {
var tableElement = document.createElement('table');
tableElement.className = (0, _classnames2['default'])('infinite-tree', 'infinite-tree-table');
var contentElement = document.createElement('tbody');
tableElement.appendChild(contentElement);
this.scrollElement.appendChild(tableElement);
this.contentElement = contentElement;
// The tag name for supporting elements
tag = 'tr';
} else {
var _contentElement = document.createElement('div');
this.scrollElement.appendChild(_contentElement);
this.contentElement = _contentElement;
// The tag name for supporting elements
tag = 'div';
}
this.scrollElement.className = (0, _classnames2['default'])('infinite-tree', 'infinite-tree-scroll');
this.contentElement.className = (0, _classnames2['default'])('infinite-tree', 'infinite-tree-content');
this.options.el.appendChild(this.scrollElement);
this.clusterize = new _clusterize2['default']({
tag: tag,
rows: [],
scrollElement: this.scrollElement,
contentElement: this.contentElement,
emptyText: this.options.noDataText,
emptyClass: this.options.noDataClass,
blocksInCluster: this.options.blocksInCluster
});
this.clusterize.on('clusterWillChange', function () {
_this2.emit('clusterWillChange');
});
this.clusterize.on('clusterDidChange', function () {
_this2.emit('clusterDidChange');
});
(0, _dom.addEventListener)(this.contentElement, 'click', this.contentListener.click);
(0, _dom.addEventListener)(this.contentElement, 'dblclick', this.contentListener.dblclick);
(0, _dom.addEventListener)(this.contentElement, 'keydown', this.contentListener.keydown);
(0, _dom.addEventListener)(this.contentElement, 'keyup', this.contentListener.keyup);
if (this.options.droppable) {
(0, _dom.addEventListener)(document, 'dragstart', this.contentListener.dragstart);
(0, _dom.addEventListener)(document, 'dragend', this.contentListener.dragend);
(0, _dom.addEventListener)(this.contentElement, 'dragenter', this.contentListener.dragenter);
(0, _dom.addEventListener)(this.contentElement, 'dragleave', this.contentListener.dragleave);
(0, _dom.addEventListener)(this.contentElement, 'dragover', this.contentListener.dragover);
(0, _dom.addEventListener)(this.contentElement, 'drop', this.contentListener.drop);
}
}
};
InfiniteTree.prototype.destroy = function destroy() {
this.clear();
if (this.options.el) {
(0, _dom.removeEventListener)(this.contentElement, 'click', this.contentListener.click);
(0, _dom.removeEventListener)(this.contentElement, 'dblclick', this.contentListener.dblclick);
(0, _dom.removeEventListener)(this.contentElement, 'keydown', this.contentListener.keydown);
(0, _dom.removeEventListener)(this.contentElement, 'keyup', this.contentListener.keyup);
if (this.options.droppable) {
(0, _dom.removeEventListener)(document, 'dragstart', this.contentListener.dragstart);
(0, _dom.removeEventListener)(document, 'dragend', this.contentListener.dragend);
(0, _dom.removeEventListener)(this.contentElement, 'dragenter', this.contentListener.dragenter);
(0, _dom.removeEventListener)(this.contentElement, 'dragleave', this.contentListener.dragleave);
(0, _dom.removeEventListener)(this.contentElement, 'dragover', this.contentListener.dragover);
(0, _dom.removeEventListener)(this.contentElement, 'drop', this.contentListener.drop);
}
if (this.clusterize) {
this.clusterize.destroy(true); // True to remove all data from the list
this.clusterize = null;
}
// Remove all child nodes
while (this.contentElement.firstChild) {
this.contentElement.removeChild(this.contentElement.firstChild);
}
while (this.scrollElement.firstChild) {
this.scrollElement.removeChild(this.scrollElement.firstChild);
}
var containerElement = this.options.el;
while (containerElement.firstChild) {
containerElement.removeChild(containerElement.firstChild);
}
this.contentElement = null;
this.scrollElement = null;
}
};
// Adds an array of new child nodes to a parent node at the specified index.
// * If the parent is null or undefined, inserts new childs at the specified index in the top-level.
// * If the parent has children, the method adds the new child to it at the specified index.
// * If the parent does not have children, the method adds the new child to the parent.
// * If the index value is greater than or equal to the number of children in the parent, the method adds the child at the end of the children.
// @param {Array} newNodes An array of new child nodes.
// @param {number} [index] The 0-based index of where to insert the child node.
// @param {Node} parentNode The Node object that defines the parent node.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.addChildNodes = function addChildNodes(newNodes, index, parentNode) {
var _this3 = this;
newNodes = [].concat(newNodes || []); // Ensure array
if (newNodes.length === 0) {
return false;
}
parentNode = parentNode || this.state.rootNode; // Defaults to rootNode if not specified
if (index === undefined) {
index = parentNode.children.length;
}
if (!ensureNodeInstance(parentNode)) {
return false;
}
// Assign parent
newNodes.forEach(function (newNode) {
newNode.parent = parentNode;
});
// Insert new child node at the specified index
parentNode.children.splice.apply(parentNode.children, [index, 0].concat(newNodes));
// Get the index of the first new node within the array of child nodes
index = parentNode.children.indexOf(newNodes[0]);
var deleteCount = parentNode.state.total;
var nodes = (0, _flattree.flatten)(parentNode.children, { openNodes: this.state.openNodes });
var rows = [];
// Update rows
rows.length = nodes.length;
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
rows[i] = this.options.rowRenderer(node, this.options);
}
if (parentNode === this.state.rootNode) {
this.nodes = nodes;
this.rows = rows;
} else {
var parentOffset = this.nodes.indexOf(parentNode);
if (parentOffset >= 0) {
if (parentNode.state.open === true) {
// Update nodes & rows
this.nodes.splice.apply(this.nodes, [parentOffset + 1, deleteCount].concat(nodes));
this.rows.splice.apply(this.rows, [parentOffset + 1, deleteCount].concat(rows));
}
// Update the row corresponding to the parent node
this.rows[parentOffset] = this.options.rowRenderer(parentNode, this.options);
}
}
// Update the lookup table with newly added nodes
parentNode.children.slice(index).forEach(function (childNode) {
_this3.flattenNode(childNode).forEach(function (node) {
if (node.id !== undefined) {
_this3.nodeTable.set(node.id, node);
}
});
});
// Update list
this.update();
return true;
};
// Adds a new child node to the end of the list of children of a specified parent node.
// * If the parent is null or undefined, inserts the child at the specified index in the top-level.
// * If the parent has children, the method adds the child as the last child.
// * If the parent does not have children, the method adds the child to the parent.
// @param {object} newNode The new child node.
// @param {Node} parentNode The Node object that defines the parent node.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.appendChildNode = function appendChildNode(newNode, parentNode) {
// Defaults to rootNode if the parentNode is not specified
parentNode = parentNode || this.state.rootNode;
if (!ensureNodeInstance(parentNode)) {
return false;
}
var index = parentNode.children.length;
var newNodes = [].concat(newNode || []); // Ensure array
return this.addChildNodes(newNodes, index, parentNode);
};
// Checks or unchecks a node.
// @param {Node} node The Node object.
// @param {boolean} [checked] Whether to check or uncheck the node. If not specified, it will toggle between checked and unchecked state.
// @return {boolean} Returns true on success, false otherwise.
// @example
//
// tree.checkNode(node); // toggle checked and unchecked state
// tree.checkNode(node, true); // checked=true, indeterminate=false
// tree.checkNode(node, false); // checked=false, indeterminate=false
//
// @doc
//
// state.checked | state.indeterminate | description
// ------------- | ------------------- | -----------
// false | false | The node and all of its children are unchecked.
// true | false | The node and all of its children are checked.
// true | true | The node will appear as indeterminate when the node is checked and some (but not all) of its children are checked.
InfiniteTree.prototype.checkNode = function checkNode(node, checked) {
if (!ensureNodeInstance(node)) {
return false;
}
this.emit('willCheckNode', node);
// Retrieve node index
var nodeIndex = this.nodes.indexOf(node);
if (nodeIndex < 0) {
error('Invalid node index');
return false;
}
if (checked === true) {
node.state.checked = true;
node.state.indeterminate = false;
} else if (checked === false) {
node.state.checked = false;
node.state.indeterminate = false;
} else {
node.state.checked = !!node.state.checked;
node.state.indeterminate = !!node.state.indeterminate;
node.state.checked = node.state.checked && node.state.indeterminate || !node.state.checked;
node.state.indeterminate = false;
}
var topmostNode = node;
var updateChildNodes = function updateChildNodes(parentNode) {
var childNode = parentNode.getFirstChild(); // Ignore parent node
while (childNode) {
// Update checked and indeterminate state
childNode.state.checked = parentNode.state.checked;
childNode.state.indeterminate = false;
if (childNode.hasChildren()) {
childNode = childNode.getFirstChild();
} else {
// Find the parent level
while (childNode !== null && childNode.getNextSibling() === null && childNode.parent !== parentNode) {
// Use child-parent link to get to the parent level
childNode = childNode.getParent();
}
// Get next sibling
if (childNode !== null) {
childNode = childNode.getNextSibling();
}
}
}
};
var updateParentNodes = function updateParentNodes(childNode) {
var parentNode = childNode.parent;
while (parentNode && parentNode.state.depth >= 0) {
topmostNode = parentNode;
var checkedCount = 0;
var indeterminate = false;
var len = parentNode.children ? parentNode.children.length : 0;
for (var i = 0; i < len; ++i) {
var _childNode = parentNode.children[i];
indeterminate = indeterminate || !!_childNode.state.indeterminate;
if (_childNode.state.checked) {
checkedCount++;
}
}
if (checkedCount === 0) {
parentNode.state.indeterminate = false;
parentNode.state.checked = false;
} else if (checkedCount > 0 && checkedCount < len || indeterminate) {
parentNode.state.indeterminate = true;
parentNode.state.checked = true;
} else {
parentNode.state.indeterminate = false;
parentNode.state.checked = true;
}
parentNode = parentNode.parent;
}
};
updateChildNodes(node);
updateParentNodes(node);
this.updateNode(topmostNode);
// Emit a "checkNode" event
this.emit('checkNode', node);
return true;
};
// Clears the tree.
InfiniteTree.prototype.clear = function clear() {
if (this.clusterize) {
this.clusterize.clear();
}
this.nodeTable.clear();
this.nodes = [];
this.rows = [];
this.state.openNodes = [];
this.state.rootNode = createRootNode(this.state.rootNode);
this.state.selectedNode = null;
};
// Closes a node to hide its children.
// @param {Node} node The Node object.
// @param {object} [options] The options object.
// @param {boolean} [options.silent] Pass true to prevent "closeNode" and "selectNode" events from being triggered.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.closeNode = function closeNode(node, options) {
var _this4 = this;
var _options = _extends({}, options),
_options$async = _options.async,
async = _options$async === undefined ? false : _options$async,
_options$asyncCallbac = _options.asyncCallback,
asyncCallback = _options$asyncCallbac === undefined ? noop : _options$asyncCallbac,
_options$silent = _options.silent,
silent = _options$silent === undefined ? false : _options$silent;
if (!ensureNodeInstance(node)) {
return false;
}
this.emit('willCloseNode', node);
// Cannot close the root node
if (node === this.state.rootNode) {
error('Cannot close the root node');
return false;
}
// Retrieve node index
if (this.nodes.indexOf(node) < 0) {
error('Invalid node index');
return false;
}
// Check if the closeNode action can be performed
if (this.state.openNodes.indexOf(node) < 0) {
return false;
}
// Toggle the collapsing state
node.state.collapsing = true;
// Update the row corresponding to the node
this.rows[this.nodes.indexOf(node)] = this.options.rowRenderer(node, this.options);
// Update list
this.update();
var fn = function fn() {
// Keep selected node unchanged if "node" is equal to "this.state.selectedNode"
if (_this4.state.selectedNode && _this4.state.selectedNode !== node) {
// row #0 - node.0 => parent node (total=4)
// row #1 - node.0.0 => close this node; next selected node (total=2)
// row #2 node.0.0.0 => selected node (total=0)
// row #3 node.0.0.1
// row #4 node.0.1
var selectedIndex = _this4.nodes.indexOf(_this4.state.selectedNode);
var _total = node.state.total;
var rangeFrom = _this4.nodes.indexOf(node) + 1;
var rangeTo = _this4.nodes.indexOf(node) + _total;
if (rangeFrom <= selectedIndex && selectedIndex <= rangeTo) {
_this4.selectNode(node, options);
}
}
node.state.open = false; // Set the open state to false
var openNodes = _this4.state.openNodes.filter(function (node) {
return node.state.open;
});
_this4.state.openNodes = openNodes;
// Subtract total from ancestor nodes
var total = node.state.total;
for (var p = node; p !== null; p = p.parent) {
p.state.total = p.state.total - total;
}
// Update nodes & rows
_this4.nodes.splice(_this4.nodes.indexOf(node) + 1, total);
_this4.rows.splice(_this4.nodes.indexOf(node) + 1, total);
// Toggle the collapsing state
node.state.collapsing = false;
// Update the row corresponding to the node
_this4.rows[_this4.nodes.indexOf(node)] = _this4.options.rowRenderer(node, _this4.options);
// Update list
_this4.update();
if (!silent) {
// Emit a "closeNode" event
_this4.emit('closeNode', node);
}
if (typeof asyncCallback === 'function') {
asyncCallback();
}
};
if (async) {
setTimeout(fn, 0);
} else {
fn();
}
return true;
};
// Filters nodes. Use a string or a function to test each node of the tree. Otherwise, it will render nothing after filtering (e.g. tree.filter(), tree.filter(null), tree.flter(0), tree.filter({}), etc.).
// @param {string|function} predicate A keyword string, or a function to test each node of the tree. If the predicate is an empty string, all nodes will be filtered. If the predicate is a function, returns true to keep the node, false otherwise.
// @param {object} [options] The options object.
// @param {boolean} [options.caseSensitive] Case sensitive string comparison. Defaults to false. This option is only available for string comparison.
// @param {boolean} [options.exactMatch] Exact string matching. Defaults to false. This option is only available for string comparison.
// @param {string} [options.filterPath] Gets the value at path of Node object. Defaults to 'name'. This option is only available for string comparison.
// @param {boolean} [options.includeAncestors] Whether to include ancestor nodes. Defaults to true.
// @param {boolean} [options.includeDescendants] Whether to include descendant nodes. Defaults to true.
// @example
//
// const filterOptions = {
// caseSensitive: false,
// exactMatch: false,
// filterPath: 'props.some.other.key',
// includeAncestors: true,
// includeDescendants: true
// };
// tree.filter('keyword', filterOptions);
//
// @example
//
// const filterOptions = {
// includeAncestors: true,
// includeDescendants: true
// };
// tree.filter(function(node) {
// const keyword = 'keyword';
// const filterText = node.name || '';
// return filterText.toLowerCase().indexOf(keyword) >= 0;
// }, filterOptions);
InfiniteTree.prototype.filter = function filter(predicate, options) {
options = _extends({
caseSensitive: false,
exactMatch: false,
filterPath: 'name',
includeAncestors: true,
includeDescendants: true
}, options);
this.filtered = true;
var rootNode = this.state.rootNode;
var traverse = function traverse(node) {
var filterNode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!node || !node.children) {
return false;
}
if (node === rootNode) {
node.state.filtered = false;
} else if (filterNode) {
node.state.filtered = true;
} else if (typeof predicate === 'string') {
// string
var filterText = (0, _utilities.get)(node, options.filterPath, '');
if (Number.isFinite(filterText)) {
filterText = String(filterText);
}
if (typeof filterText !== 'string') {
filterText = '';
}
var keyword = predicate;
if (!options.caseSensitive) {
filterText = filterText.toLowerCase();
keyword = keyword.toLowerCase();
}
node.state.filtered = options.exactMatch ? filterText === keyword : filterText.indexOf(keyword) >= 0;
} else if (typeof predicate === 'function') {
// function
var callback = predicate;
node.state.filtered = !!callback(node);
} else {
node.state.filtered = false;
}
if (options.includeDescendants) {
filterNode = filterNode || node.state.filtered;
}
var filtered = false;
for (var i = 0; i < node.children.length; ++i) {
var childNode = node.children[i];
if (!childNode) {
continue;
}
if (traverse(childNode, filterNode)) {
filtered = true;
}
}
if (options.includeAncestors && filtered) {
node.state.filtered = true;
}
return node.state.filtered;
};
traverse(rootNode);
// Update rows
this.rows.length = this.nodes.length;
for (var i = 0; i < this.nodes.length; ++i) {
var node = this.nodes[i];
this.rows[i] = this.options.rowRenderer(node, this.options);
}
this.update();
};
// Flattens all child nodes of a parent node by performing full tree traversal using child-parent link.
// No recursion or stack is involved.
// @param {Node} parentNode The Node object that defines the parent node.
// @return {array} Returns an array of Node objects containing all the child nodes of the parent node.
InfiniteTree.prototype.flattenChildNodes = function flattenChildNodes(parentNode) {
// Defaults to rootNode if the parentNode is not specified
parentNode = parentNode || this.state.rootNode;
if (!ensureNodeInstance(parentNode)) {
return [];
}
var list = [];
var node = parentNode.getFirstChild(); // Ignore parent node
while (node) {
list.push(node);
if (node.hasChildren()) {
node = node.getFirstChild();
} else {
// Find the parent level
while (node !== null && node.getNextSibling() === null && node.parent !== parentNode) {
// Use child-parent link to get to the parent level
node = node.getParent();
}
// Get next sibling
if (node !== null) {
node = node.getNextSibling();
}
}
}
return list;
};
// Flattens a node by performing full tree traversal using child-parent link.
// No recursion or stack is involved.
// @param {Node} node The Node object.
// @return {array} Returns a flattened list of Node objects.
InfiniteTree.prototype.flattenNode = function flattenNode(node) {
if (!ensureNodeInstance(node)) {
return [];
}
return [node].concat(this.flattenChildNodes(node));
};
// Gets a list of child nodes.
// @param {Node} [parentNode] The Node object that defines the parent node. If null or undefined, returns a list of top level nodes.
// @return {array} Returns an array of Node objects containing all the child nodes of the parent node.
InfiniteTree.prototype.getChildNodes = function getChildNodes(parentNode) {
// Defaults to rootNode if the parentNode is not specified
parentNode = parentNode || this.state.rootNode;
if (!ensureNodeInstance(parentNode)) {
return [];
}
return parentNode.children;
};
// Gets a node by its unique id. This assumes that you have given the nodes in the data a unique id.
// @param {string|number} id An unique node id. A null value will be returned if the id doesn't match.
// @return {Node} Returns a node the matches the id, null otherwise.
InfiniteTree.prototype.getNodeById = function getNodeById(id) {
var node = this.nodeTable.get(id);
if (!node) {
// Find the first node that matches the id
node = this.nodes.filter(function (node) {
return node.id === id;
})[0];
if (!node) {
return null;
}
this.nodeTable.set(node.id, node);
}
return node;
};
// Returns the node at the specified point. If the specified point is outside the visible bounds or either coordinate is negative, the result is null.
// @param {number} x A horizontal position within the current viewport.
// @param {number} y A vertical position within the current viewport.
// @return {Node} The Node object under the given point.
InfiniteTree.prototype.getNodeFromPoint = function getNodeFromPoint(x, y) {
var el = document.elementFromPoint(x, y);
while (el && el.parentElement !== this.contentElement) {
el = el.parentElement;
}
if (!el) {
return null;
}
var id = el.getAttribute(this.options.nodeIdAttr);
var node = this.getNodeById(id);
return node;
};
// Gets an array of open nodes.
// @return {array} Returns an array of Node objects containing open nodes.
InfiniteTree.prototype.getOpenNodes = function getOpenNodes() {
// returns a shallow copy of an array into a new array object.
return this.state.openNodes.slice();
};
// Gets the root node.
// @return {Node} Returns the root node, or null if empty.
InfiniteTree.prototype.getRootNode = function getRootNode() {
return this.state.rootNode;
};
// Gets the selected node.
// @return {Node} Returns the selected node, or null if not selected.
InfiniteTree.prototype.getSelectedNode = function getSelectedNode() {
return this.state.selectedNode;
};
// Gets the index of the selected node.
// @return {number} Returns the index of the selected node, or -1 if not selected.
InfiniteTree.prototype.getSelectedIndex = function getSelectedIndex() {
return this.nodes.indexOf(this.state.selectedNode);
};
// Inserts the specified node after the reference node.
// @param {object} newNode The new sibling node.
// @param {Node} referenceNode The Node object that defines the reference node.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.insertNodeAfter = function insertNodeAfter(newNode, referenceNode) {
if (!ensureNodeInstance(referenceNode)) {
return false;
}
var parentNode = referenceNode.getParent();
var index = parentNode.children.indexOf(referenceNode) + 1;
var newNodes = [].concat(newNode || []); // Ensure array
return this.addChildNodes(newNodes, index, parentNode);
};
// Inserts the specified node before the reference node.
// @param {object} newNode The new sibling node.
// @param {Node} referenceNode The Node object that defines the reference node.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.insertNodeBefore = function insertNodeBefore(newNode, referenceNode) {
if (!ensureNodeInstance(referenceNode)) {
return false;
}
var parentNode = referenceNode.getParent();
var index = parentNode.children.indexOf(referenceNode);
var newNodes = [].concat(newNode || []); // Ensure array
return this.addChildNodes(newNodes, index, parentNode);
};
// Loads data in the tree.
// @param {object|array} data The data is an object or array of objects that defines the node.
InfiniteTree.prototype.loadData = function loadData() {
var _this5 = this;
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
this.nodes = (0, _flattree.flatten)(data, { openAllNodes: this.options.autoOpen });
// Clear lookup table
this.nodeTable.clear();
this.state.openNodes = this.nodes.filter(function (node) {
return node.state.open;
});
this.state.selectedNode = null;
var rootNode = function () {
var node = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
// Finding the root node
while (node && node.parent !== null) {
node = node.parent;
}
return node;
}(this.nodes.length > 0 ? this.nodes[0] : null);
this.state.rootNode = rootNode || createRootNode(this.state.rootNode); // Create a new root node if rootNode is null
// Update the lookup table with newly added nodes
this.flattenChildNodes(this.state.rootNode).forEach(function (node) {
if (node.id !== undefined) {
_this5.nodeTable.set(node.id, node);
}
});
// Update rows
this.rows.length = this.nodes.length;
for (var i = 0; i < this.nodes.length; ++i) {
var node = this.nodes[i];
this.rows[i] = this.options.rowRenderer(node, this.options);
}
// Update list
this.update();
};
// Moves a node from its current position to the new position.
// @param {Node} node The Node object.
// @param {Node} parentNode The Node object that defines the parent node.
// @param {number} [index] The 0-based index of where to insert the child node.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.moveNodeTo = function moveNodeTo(node, parentNode, index) {
if (!ensureNodeInstance(node) || !ensureNodeInstance(parentNode)) {
return false;
}
for (var p = parentNode; p !== null; p = p.parent) {
if (p === node) {
error('Cannot move an ancestor node (id=' + node.id + ') to the specified parent node (id=' + parentNode.id + ').');
return false;
}
}
return this.removeNode(node) && this.addChildNodes(node, index, parentNode);
};
// Opens a node to display its children.
// @param {Node} node The Node object.
// @param {object} [options] The options object.
// @param {boolean} [options.silent] Pass true to prevent "openNode" event from being triggered.
// @return {boolean} Returns true on success, false otherwise.
InfiniteTree.prototype.openNode = function openNode(node, options) {
var _this6 = this;
var _options2 = _extends({}, options),
_options2$async = _options2.async,
async = _options2$async === undefined ? false : _options2$async,
_options2$asyncCallba = _options2.asyncCallback,
asyncCallback = _options2$asyncCallba === undefined ? noop : _options2$asyncCallba,
_options2$silent = _options2.silent,
silent = _options2$silent === undefined ? false : _options2$silent;
if (!ensureNodeInstance(node)) {
return false;
}
if (!this.nodeTable.has(node.id)) {
error('Cannot open node with the given node id:', node.id);
return false;
}
// Check if the openNode action can be performed
if (this.state.openNodes.indexOf(node) >= 0) {
return false;
}
this.emit('willOpenNode', node);
// Retrieve node index
var fn = function fn() {
node.state.open = true;
if (_this6.state.openNodes.indexOf(node) < 0) {
// the most recently used items first
_this6.state.openNodes = [node].concat(_this6.state.openNodes);
}
var nodes = (0, _flattree.flatten)(node.children, { openNodes: _this6.state.openNodes });
// Add all child nodes to the lookup table if the first child does not exist in the lookup table
if (nodes.length > 0 && !_this6.nodeTable.get(nodes[0])) {
nodes.forEach(function (node) {
if (node.id !== undefined) {
_this6.nodeTable.set(node.id, node);
}
});
}
// Toggle the expanding state
node.state.expanding = false;
if (_this6.nodes.indexOf(node) >= 0) {
var rows = [];
// Update rows
rows.length = nodes.length;
for (var i = 0; i < nodes.length; ++i) {
var _node = nodes[i];
rows[i] = _this6.options.rowRenderer(_node, _this6.options);
}
// Update nodes & rows
_this6.nodes.splice.apply(_this6.nodes, [_this6.nodes.indexOf(node) + 1, 0].concat(nodes));
_this6.rows.splice.apply(_this6.rows, [_this6.nodes.indexOf(node) + 1, 0].concat(rows));
// Update the row corresponding to the node
_this6.rows[_this6.nodes.indexOf(node)] = _this6.options.rowRenderer(node, _this6.options);
// Update list
_this6.update();
}
if (!silent