'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,
            droppable: false,
            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
            });

            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;
        }

        if ((typeof index === 'undefined' ? 'undefined' : _typeof(index)) === 'object') {
            // The 'object' type might be Node or null
            parentNode = index || this.state.rootNode; // Defaults to rootNode if not specified
            index = parentNode.children.length;
        } else {
            parentNode = parentNode || this.state.rootNode; // Defaults to rootNode if not specified
        }

        if (!ensureNodeInstance(parentNode)) {
            return false;
        }

        if (typeof index !== 'number') {
            index = parentNode.children.length;
        }

        // 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) {
                // 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);
    };
    // 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);

        // Retrieve node index
        var nodeIndex = this.nodes.indexOf(node);
        if (nodeIndex < 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[nodeIndex] = 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 = nodeIndex + 1;
                var rangeTo = nodeIndex + _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.hasChildren() && 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(nodeIndex + 1, total);
            _this4.rows.splice(nodeIndex + 1, total);

            // Toggle the collapsing state
            node.state.collapsing = false;
            // Update the row corresponding to the node
            _this4.rows[nodeIndex] = _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.getNextSibling() === null && node.parent !== parentNode) {
                    // Use child-parent link to get to the parent level
                    node = node.getParent();
                }

                // Get next sibling
                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.hasChildren() && 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;
        }

        this.emit('willOpenNode', node);

        // Retrieve node index
        var nodeIndex = this.nodes.indexOf(node);
        if (nodeIndex < 0) {
            error('Invalid node index');
            return false;
        }

        // Check if the openNode action can be performed
        if (this.state.openNodes.indexOf(node) >= 0) {
            return false;
        }

        if (!node.hasChildren() && node.loadOnDemand) {
            if (typeof this.options.loadNodes !== 'function') {
                return false;
            }

            // Reentrancy not allowed
            if (node.state.loading === true) {
                return false;
            }

            // Toggle the loading state
            node.state.loading = true;
            // Update the row corresponding to the node
            this.rows[nodeIndex] = this.options.rowRenderer(node, this.options);
            // Update list
            this.update();

            // Do a setTimeout to prevent the CPU intensive task
            setTimeout(function () {
                _this6.options.loadNodes(node, function (err, nodes) {
                    var done = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop;

                    nodes = (0, _ensureArray2['default'])(nodes);

                    if (err || nodes.length === 0) {
                        // Toggle the loading state
                        node.state.loading = false;
                        // Update the row corresponding to the node
                        _this6.rows[nodeIndex] = _this6.options.rowRenderer(node, _this6.options);
                        // Update list
                        _this6.update();

                        if (typeof done === 'function') {
                            done();
                        }
                        return;
                    }

                    _this6.addChildNodes(nodes, node);

                    // Ensure the node has children to prevent infinite loop
                    if (node.hasChildren()) {
                        // Call openNode again
                        _this6.openNode(node, _extends({}, options, {
                            async: true,
                            asyncCallback: function asyncCallback() {
                                // Toggle the loading state
                                node.state.loading = false;
                                // Update the row corresponding to the node
                                _this6.rows[nodeIndex] = _this6.options.rowRenderer(node, _this6.options);
                                // Update list
                                _this6.update();

                                if (typeof done === 'function') {
                                    done();
                                }
                            }
                        }));
                    } else {
                        // Toggle the loading state
                        node.state.loading = false;
                        // Update the row corresponding to the node
                        _this6.rows[nodeIndex] = _this6.options.rowRenderer(node, _this6.options);
                        // Update list
                        _this6.update();

                        if (typeof done === 'function') {
                            done();
                        }
                    }
                });
            }, 0);

            return true;
        }

        // Toggle the expanding state
        node.state.expanding = true;
        // Update the row corresponding to the node
        this.rows[nodeIndex] = this.options.rowRenderer(node, this.options);
        // Update list
        this.update();

        var fn = function fn() {
            node.state.open = true; // Set node.state.open to true
            var openNodes = [node].concat(_this6.state.openNodes); // the most recently used items first
            _this6.state.openNodes = openNodes;

            var nodes = (0, _flattree.flatten)(node.children, { openNodes: _this6.state.openNodes });
            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, [nodeIndex + 1, 0].concat(nodes));
            _this6.rows.splice.apply(_this6.rows, [nodeIndex + 1, 0].concat(rows));

            // Update the row corresponding to the node
            _this6.rows[nodeIndex] = _this6.options.rowRenderer(node, _this6.options);

            // 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;
            // Update the row corresponding to the node
            _this6.rows[nodeIndex] = _this6.options.rowRenderer(node, _this6.options);
            // Update list
            _this6.update();

            if (!silent) {
                // Emit a "openNode" event
                _this6.emit('openNode', node);
            }

            if (typeof asyncCallback === 'function') {
                asyncCallback();
            }
        };

        if (async) {
            setTimeout(fn, 0);
        } else {
            fn();
        }

        return true;
    };
    // Removes all child nodes from a parent node.
    // @param {Node} parentNode The Node object that defines the parent node.
    // @param {object} [options] The options object.
    // @param {boolean} [options.silent] Pass true to prevent "selectNode" event from being triggered.
    // @return {boolean} Returns true on success, false otherwise.


    InfiniteTree.prototype.removeChildNodes = function removeChildNodes(parentNode, options) {
        var _this7 = this;

        if (!ensureNodeInstance(parentNode)) {
            return false;
        }

        if (parentNode.children.length === 0) {
            return false;
        }
        if (parentNode === this.state.rootNode) {
            this.clear();
            return true;
        }

        var parentNodeIndex = this.nodes.indexOf(parentNode);

        // Update selected node
        if (parentNodeIndex >= 0 && this.state.selectedNode) {
            // row #0 - node.0         => parent node (total=4)
            // row #1   - node.0.0
            // row #2       node.0.0.0 => current selected node
            // row #3       node.0.0.1
            // row #4     node.0.1
            var selectedIndex = this.nodes.indexOf(this.state.selectedNode);
            var rangeFrom = parentNodeIndex + 1;
            var rangeTo = parentNodeIndex + parentNode.state.total;

            if (rangeFrom <= selectedIndex && selectedIndex <= rangeTo) {
                if (parentNode === this.state.rootNode) {
                    this.selectNode(null, options);
                } else {
                    this.selectNode(parentNode, options);
                }
            }
        }

        // Get the nodes being removed
        var removedNodes = this.flattenChildNodes(parentNode);

        // Get the number of nodes to be removed
        var deleteCount = parentNode.state.total;

        // Subtract the deleteCount for all ancestors (parent, grandparent, etc.) of the current node
        for (var p = parentNode; p !== null; p = p.parent) {
            p.state.total = p.state.total - deleteCount;
        }

        // Update parent node
        parentNode.children = [];
        parentNode.state.open = parentNode.state.open && parentNode.children.length > 0;

        if (parentNodeIndex >= 0) {
            // Update nodes & rows
            this.nodes.splice(parentNodeIndex + 1, deleteCount);
            this.rows.splice(parentNodeIndex + 1, deleteCount);

            // Update the row corresponding to the parent node
            this.rows[parentNodeIndex] = this.options.rowRenderer(parentNode, this.options);
        }

        {
            // Update open nodes and lookup table
            this.state.openNodes = this.state.openNodes.filter(function (node) {
                return removedNodes.indexOf(node) < 0 && node.hasChildren() && node.state.open;
            });

            removedNodes.forEach(function (node) {
                _this7.nodeTable.unset(node.id);
            });
        }

        // Update list
        this.update();

        return true;
    };
    // Removes a node and all of its child nodes.
    // @param {Node} node The Node object.
    // @param {object} [options] The options object.
    // @param {boolean} [options.silent] Pass true to prevent "selectNode" event from being triggered.
    // @return {boolean} Returns true on success, false otherwise.


    InfiniteTree.prototype.removeNode = function removeNode(node, options) {
        var _this8 = this;

        if (!ensureNodeInstance(node)) {
            return false;
        }

        var parentNode = node.parent;
        if (!parentNode) {
            return false;
        }

        // Retrieve node index
        var nodeIndex = this.nodes.indexOf(node);
        var parentNodeIndex = this.nodes.indexOf(parentNode);

        // Update selected node
        if (nodeIndex >= 0 && this.state.selectedNode) {
            // row #0 - node.0         => parent node (total=4)
            // row #1   - node.0.0     => remove this node (total=2)
            // row #2       node.0.0.0 => current selected node (total=0)
            // row #3       node.0.0.1
            // row #4     node.0.1     => next selected node (total=0)
            var selectedIndex = this.nodes.indexOf(this.state.selectedNode);
            var rangeFrom = nodeIndex;
            var rangeTo = nodeIndex + node.state.total + 1;

            if (rangeFrom <= selectedIndex && selectedIndex <= rangeTo) {
                // Change the selected node in the following order:
                // 1. next sibling node
                // 2. previous sibling node
                // 3. parent node
                var selectedNode = node.getNextSibling() || node.getPreviousSibling() || node.getParent();

                if (selectedNode === this.state.rootNode) {
                    this.selectNode(null, options);
                } else {
                    this.selectNode(selectedNode, options);
                }
            }
        }

        // Get the nodes being removed
        var removedNodes = this.flattenNode(node);

        // Get the number of nodes to be removed
        var deleteCount = node.state.total + 1;

        // Subtract the deleteCount for all ancestors (parent, grandparent, etc.) of the current node
        for (var p = parentNode; p !== null; p = p.parent) {
            p.state.total = p.state.total - deleteCount;
        }

        // Update parent node
        parentNode.children.splice(parentNode.children.indexOf(node), 1);
        parentNode.state.open = parentNode.state.open && parentNode.children.length > 0;

        if (nodeIndex >= 0) {
            // Update nodes & rows
            this.nodes.splice(nodeIndex, deleteCount);
            this.rows.splice(nodeIndex, deleteCount);
        }

        // Update the row corresponding to the parent node
        if (parentNodeIndex >= 0) {
            this.rows[parentNodeIndex] = this.options.rowRenderer(parentNode, this.options);
        }

        {
            // Update open nodes and lookup table
            this.state.openNodes = this.state.openNodes.filter(function (node) {
                return removedNodes.indexOf(node) < 0 && node.hasChildren() && node.state.open;
            });

            removedNodes.forEach(function (node) {
                _this8.nodeTable.unset(node.id);
            });
        }

        // Update list
        this.update();

        return true;
    };
    // Sets the current scroll position to this node.
    // @param {Node} node The Node object.
    // @return {boolean} Returns true on success, false otherwise.


    InfiniteTree.prototype.scrollToNode = function scrollToNode(node) {
        if (!ensureNodeInstance(node)) {
            return false;
        }

        // Retrieve node index
        var nodeIndex = this.nodes.indexOf(node);
        if (nodeIndex < 0) {
            return false;
        }
        if (!this.contentElement) {
            return false;
        }

        // Scroll to a desired position
        var firstChild = this.contentElement.firstChild;
        while (firstChild) {
            var className = firstChild.className || '';
            if (className.indexOf('clusterize-extra-row') < 0 && firstChild.offsetHeight > 0) {
                break;
            }
            firstChild = firstChild.nextSibling;
        }
        // If all items in the list is the same height, it can be calculated by nodeIndex * height.
        var offsetHeight = firstChild && firstChild.offsetHeight || 0;
        if (offsetHeight > 0) {
            this.scrollTop(nodeIndex * offsetHeight);
        }

        // Find the absolute position of the node
        var nodeSelector = '[' + this.options.nodeIdAttr + '="' + node.id + '"]';
        var nodeEl = this.contentElement.querySelector(nodeSelector);
        if (nodeEl) {
            this.scrollTop(nodeEl.offsetTop);
        }

        return true;
    };
    // Gets (or sets) the current vertical position of the scroll bar.
    // @param {number} [value] If the value is specified, indicates the new position to set the scroll bar to.
    // @return {number} Returns the vertical scroll position.


    InfiniteTree.prototype.scrollTop = function scrollTop(value) {
        if (!this.scrollElement) {
            return 0;
        }
        if (value !== undefined) {
            this.scrollElement.scrollTop = Number(value);
        }
        return this.scrollElement.scrollTop;
    };
    // Selects a node.
    // @param {Node} node The Node object. If null or undefined, deselects the current node.
    // @param {object} [options] The options object.
    // @param {boolean} [options.autoScroll] Pass true to automatically scroll to the selected node. Defaults to true.
    // @param {boolean} [options.silent] Pass true to prevent "selectNode" event from being triggered. Defaults to false.
    // @return {boolean} Returns true on success, false otherwise.


    InfiniteTree.prototype.selectNode = function selectNode() {
        var node = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
        var options = arguments[1];
        var _options3 = this.options,
            selectable = _options3.selectable,
            shouldSelectNode = _options3.shouldSelectNode;

        var _options4 = _extends({}, options),
            _options4$autoScroll = _options4.autoScroll,
            autoScroll = _options4$autoScroll === undefined ? true : _options4$autoScroll,
            _options4$silent = _options4.silent,
            silent = _options4$silent === undefined ? false : _options4$silent;

        this.emit('willSelectNode', node);

        if (!selectable) {
            return false;
        }
        if (typeof shouldSelectNode === 'function' && !shouldSelectNode(node)) {
            return false;
        }
        if (node === this.state.rootNode) {
            return false;
        }

        if (node === null) {
            // Deselect the current node
            if (this.state.selectedNode) {
                var selectedNode = this.state.selectedNode;
                var selectedIndex = this.nodes.indexOf(this.state.selectedNode);

                selectedNode.state.selected = false;
                this.rows[selectedIndex] = this.options.rowRenderer(selectedNode, this.options);
                this.state.selectedNode = null;

                // Update list
                this.update();

                if (!silent) {
                    // Emit a "selectNode" event
                    this.emit('selectNode', null);
                }

                return true;
            }

            return false;
        }

        if (!ensureNodeInstance(node)) {
            return false;
        }

        // Retrieve node index
        var nodeIndex = this.nodes.indexOf(node);
        if (nodeIndex < 0) {
            error('Invalid node index');
            return false;
        }

        // Select this node
        if (this.state.selectedNode !== node) {
            node.state.selected = true;

            // Update the row corresponding to the node
            this.rows[nodeIndex] = this.options.rowRenderer(node, this.options);
        }

        // Deselect the current node
        if (this.state.selectedNode) {
            var _selectedNode = this.state.selectedNode;
            var _selectedIndex = this.nodes.indexOf(this.state.selectedNode);
            _selectedNode.state.selected = false;
            this.rows[_selectedIndex] = this.options.rowRenderer(_selectedNode, this.options);
        }

        if (this.state.selectedNode !== node) {
            this.state.selectedNode = node;

            if (!silent) {
                // Emit a "selectNode" event
                this.emit('selectNode', node);
            }

            if (autoScroll && this.scrollElement && this.contentElement) {
                var nodeSelector = '[' + this.options.nodeIdAttr + '="' + node.id + '"]';
                var nodeEl = this.contentElement.querySelector(nodeSelector);
                if (nodeEl) {
                    var offsetTop = nodeEl.offsetTop || 0;
                    var offsetHeight = nodeEl.offsetHeight || 0;

                    // Scroll Up
                    if (offsetTop < this.scrollElement.scrollTop) {
                        this.scrollElement.scrollTop = offsetTop;
                    }

                    // Scroll Down
                    if (offsetTop + offsetHeight >= this.scrollElement.scrollTop + this.scrollElement.clientHeight) {
                        this.scrollElement.scrollTop += offsetHeight;
                    }
                }
            }
        } else {
            this.state.selectedNode = null;

            if (!silent) {
                // Emit a "selectNode" event
                this.emit('selectNode', null);
            }
        }

        // Update list
        this.update();

        return true;
    };
    // Swaps two nodes.
    // @param {Node} node1 The Node object.
    // @param {Node} node2 The Node object.
    // @return {boolean} Returns true on success, false otherwise.


    InfiniteTree.prototype.swapNodes = function swapNodes(node1, node2) {
        if (!ensureNodeInstance(node1) || !ensureNodeInstance(node1.parent)) {
            return false;
        }
        if (!ensureNodeInstance(node2) || !ensureNodeInstance(node2.parent)) {
            return false;
        }

        var parentNode1 = node1.parent;
        var parentNode2 = node2.parent;

        for (var p = parentNode1; p !== null; p = p.parent) {
            if (p === node2) {
                error('Cannot swap two nodes with one being an ancestor of the other.');
                return false;
            }
        }
        for (var _p = parentNode2; _p !== null; _p = _p.parent) {
            if (_p === node1) {
                error('Cannot swap two nodes with one being an ancestor of the other.');
                return false;
            }
        }

        var nodeIndex1 = parentNode1.children.indexOf(node1);
        var nodeIndex2 = parentNode2.children.indexOf(node2);

        return this.moveNodeTo(node1, parentNode2, nodeIndex2) && this.moveNodeTo(node2, parentNode1, nodeIndex1);
    };
    // Toggles a node to display or hide its children.
    // @param {Node} node The Node object.
    // @param {object} [options] The options object.
    // @param {boolean} [options.silent] Pass true to prevent "closeNode", "openNode", and "selectNode" events from being triggered.
    // @return {boolean} Returns true on success, false otherwise.


    InfiniteTree.prototype.toggleNode = function toggleNode(node, options) {
        if (!ensureNodeInstance(node)) {
            return false;
        }

        if (this.state.openNodes.indexOf(node) >= 0) {
            // Close node
            return this.closeNode(node, options);
        } else {
            // Open node
            return this.openNode(node, options);
        }
    };
    // Serializes the current state of a node to a JSON string.
    // @param {Node} node The Node object. If null, returns the whole tree.
    // @return {string} Returns a JSON string represented the tree.


    InfiniteTree.prototype.toString = function toString() {
        var node = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;

        var traverse = function traverse(node) {
            var s = '[';
            if (node && node.children) {
                var _loop = function _loop(i) {
                    var list = [];
                    s = s + '{';
                    Object.keys(node).forEach(function (key) {
                        var value = node[key];
                        if (key === 'parent') {
                            // ignore parent
                            return;
                        }
                        if (key === 'children') {
                            // traverse child nodes
                            list.push('"' + key + '":' + traverse(node.children[i]));
                            return;
                        }
                        if (typeof value === 'string' || (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') {
                            list.push('"' + key + '":' + JSON.stringify(value));
                        } else {
                            // primitive types
                            list.push('"' + key + '":' + value);
                        }
                    });
                    s = s + list.join(',');
                    s = s + '}' + (i === node.children.length - 1 ? '' : ',');
                };

                for (var i = 0; i < node.children.length; ++i) {
                    _loop(i);
                }
            }
            s = s + ']';
            return s;
        };

        if (!node) {
            node = this.state.rootNode;
        }

        return traverse(node);
    };
    // Unfilters nodes.


    InfiniteTree.prototype.unfilter = function unfilter() {
        this.filtered = false;

        var rootNode = this.state.rootNode;
        var traverse = function traverse(node) {
            if (!node) {
                return;
            }
            delete node.state.filtered;

            if (!node.children) {
                return;
            }
            for (var i = 0; i < node.children.length; ++i) {
                var childNode = node.children[i];
                if (!childNode) {
                    continue;
                }
                traverse(childNode);
            }
        };

        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();
    };
    // Updates the tree.


    InfiniteTree.prototype.update = function update() {
        // Emit a "contentWillUpdate" event
        this.emit('contentWillUpdate');

        if (this.clusterize) {
            // Update list
            var rows = this.rows.filter(function (row) {
                return !!row;
            });
            this.clusterize.update(rows);
        }

        // Emit a "contentWillUpdate" event
        this.emit('contentDidUpdate');
    };
    // Updates the data of a node.
    // @param {Node} node The Node object.
    // @param {object} data The data object.
    // @param {object} [options] The options object.
    // @param {boolean} [options.shallowRendering] True to render only the parent node, false to render the parent node and all expanded child nodes. Defaults to false.


    InfiniteTree.prototype.updateNode = function updateNode(node, data, options) {
        if (!ensureNodeInstance(node)) {
            return;
        }

        // Clone a new one
        data = _extends({}, data);

        if (data.id !== undefined && data.id !== null) {
            this.nodeTable.unset(node.id);
            this.nodeTable.set(data.id, node);
            node.id = data.id;
        }

        // Ignore keys: id, children, parent, and state
        delete data.id;
        delete data.children;
        delete data.parent;
        delete data.state;

        node = (0, _extend2['default'])(node, data);

        // Retrieve node index
        var nodeIndex = this.nodes.indexOf(node);
        if (nodeIndex >= 0) {
            var _options5 = _extends({}, options),
                _options5$shallowRend = _options5.shallowRendering,
                shallowRendering = _options5$shallowRend === undefined ? false : _options5$shallowRend;

            // Update the row corresponding to the node


            this.rows[nodeIndex] = this.options.rowRenderer(node, this.options);

            if (!shallowRendering) {
                var total = node.state.total;
                var rangeFrom = nodeIndex + 1;
                var rangeTo = nodeIndex + total;
                for (var index = rangeFrom; index <= rangeTo; ++index) {
                    this.rows[index] = this.options.rowRenderer(this.nodes[index], this.options);
                }
            }

            // Update list
            this.update();
        }
    };

    return InfiniteTree;
}(_events2['default'].EventEmitter);

exports['default'] = InfiniteTree;