UNPKG

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
'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