Source: core/tree-controller.js

var Montage = require("./core").Montage,
    WeakMap = require("collections/weak-map"),
    parse = require("frb/parse"),
    evaluate = require("frb/evaluate");

var TreeNode = exports.TreeNode = Montage.specialize({

    constructor: {
        value: function (data, controller) {
            this.data = data;
            this.controller = controller;
        }
    },

    isExpanded: {
        get: function () {
            return this.controller.isNodeExpanded(this.data);
        },
        set: function (value) {
            if (value) {
                this.controller.expandNode(this.data);
            } else {
                this.controller.collapseNode(this.data);
            }
        }
    }

});


exports.TreeController = Montage.specialize({

    constructor: {
        value: function () {
            this._listenersHash = {};
            this._listenersCounter = 0;
        }
    },

    _childrenExpressionProperty: {
        value: "children"
    },

    _childrenExpression: {
        value: null
    },

    /**
     * An FRB expression, that evaluated against content or any of its
     * children, produces an array of that content's children.
     * By default it is "children"
     */
    childrenExpression: {
        get: function () {
            if (this._childrenExpression === null) {
                return "children";
            }
            return this._childrenExpression;
        },
        set: function (value) {
            if (this._childrenExpression !== value) {
                var parsedValue = null;

                this._childrenExpression = value;
                if (value) {

                    // We parse the given childrenExpression...

                    if (typeof value === "string") {
                        parsedValue = parse(value);
                    }

                    // ...and if it is just a property we set _childrenExpressionProperty
                    //  to be used later for speed optimisation purposes

                    if ((parsedValue !== null) &&
                        (parsedValue.type === "property") &&
                        (parsedValue.args) &&
                        (parsedValue.args.length === 2) &&
                        (parsedValue.args[0].type === "value") &&
                        (parsedValue.args[1].type === "literal")) {
                        this._childrenExpressionProperty = parsedValue.args[1].value;
                    } else {
                        this._childrenExpressionProperty = null;
                    }
                } else {
                    this._childrenExpressionProperty = "children";
                }
            }
        }
    },

    /**
     * Whether nodes in the tree should be expanded by default.
     * This will only apply when the tree's [data]{@link TreeController#data}
     * property is set. Nodes added dynamically will ignore this flag.
     *
     * @type boolean
     */
    initiallyExpanded: {
        value: false
    },

    _data: {
        value: null
    },

    /**
     * Tree data model / root
     */
    data: {
        get: function () {
            return this._data;
        },
        set: function (value) {
            if (this._data !== value) {
                this._expansionMap = new WeakMap();
                this._data = value;
                if (this.initiallyExpanded) {
                    this.expandAll();
                } else {
                    this.handleTreeChange();
                }
            }
        }
    },

    /**
     * Calls handleTreeChange in delegate when nodes
     * are expanded/collapsed and when data changes
     */
    handleTreeChange: {
        value: function () {
            if (!this._isOwnUpdate) {
                this._updateListeners();
                if (this.delegate && this.delegate.handleTreeChange) {
                    this.delegate.handleTreeChange();
                }
            }
        }
    },

    _expandNode: {
        value: function (node) {
            if (!this.isNodeExpanded(node)) {
                this._expansionMap.set(node, {});
                return true;
            }
            return false;
        }
    },

    /**
     * Expands a given node of the tree
     */
    expandNode: {
        value: function (node) {
            if (typeof node === "object" && this._expandNode(node)) {
                this.handleTreeChange();
                return true;
            }
            return false;
        }
    },

    _expandAll: {
        value: function (node) {
            var childrenData = this.childrenFromNode(node),
                length,
                i;

            if (childrenData) {
                length = childrenData.length;
                if (length) {
                    this._expandNode(node);
                    for (i = 0; i < length; i++) {
                        this._expandAll(childrenData[i]);
                    }
                }
            }
        }
    },

    /**
     * Expands all nodes with children in the tree.
     */
    expandAll: {
        value: function () {
            if (this._data) {
                this._expandAll(this._data);
                this.handleTreeChange();
            }
        }
    },

    _collapseNode: {
        value: function (node) {
            return this._expansionMap.delete(node);
        }
    },

    /**
     * Collapses a given node of the tree
     */
    collapseNode: {
        value: function (node) {
            if (typeof node === "object" && this._collapseNode(node)) {
                this.handleTreeChange();
                return true;
            }
            return false;
        }
    },

    /**
     * Collapses all nodes with children in the tree.
     */
    collapseAll: {
        value: function () {
            this._expansionMap = new WeakMap();
            this.handleTreeChange();
            return true;
        }
    },

    /**
     * Gets the node expansion value - boolean - for a given node
     */
    isNodeExpanded: {
        value: function (node) {
            return this._expansionMap.has(node);
        }
    },

    /**
     * Returns the children of a given node based on childrenExpression
     */
    childrenFromNode: {
        value: function (node) {

            // This is a speed optimisation. If childrenExpression
            // is just a single property, we don't evaluate it

            if (this._childrenExpressionProperty === null) {
                return evaluate(this._childrenExpression, node);
            }
            return node[this._childrenExpressionProperty];
        }
    },

    _getReachableExpandedNodes: {
        value: function (node, result) {
            var expansionMetadata,
                children,
                length,
                i;

            if (!result) {
                result = [];
            }
            if (node && (expansionMetadata = this._expansionMap.get(node))) {
                result.push(node);
                children = this.childrenFromNode(node);
                if (children) {
                    length = children.length;
                    for (i = 0; i < length; i++) {
                        this._getReachableExpandedNodes(children[i], result);
                    }
                }
            }
            return result;
        }
    },

    _addListener: {
        value: function (expandedNode) {
            var expansionMetadata = this._expansionMap.get(expandedNode),
                treeNode,
                cancelListener;

            this._isOwnUpdate = true;
            treeNode = new TreeNode(expandedNode, this);
            cancelListener = treeNode.addRangeAtPathChangeListener(
                this._childrenExpression ? "data.path(controller._childrenExpression)" : "data.children",
                this,
                "handleTreeChange"
            );
            this._isOwnUpdate = false;
            this._listenersCounter++;
            this._listenersHash[this._listenersCounter] = {
                cancelListener: cancelListener,
                node: expandedNode
            };
            expansionMetadata.listenerId = this._listenersCounter;
            return this._listenersCounter;
        }
    },

    _removeListener: {
        value: function (id) {
            var expandedNode = this._listenersHash[id].node,
                expansionMetadata = this._expansionMap.get(expandedNode);

            this._listenersHash[id].cancelListener();
            if (expansionMetadata) {
                delete expansionMetadata.listenerId;
            }
        }
    },

    _updateListeners: {
        value: function () {
            var reachableExpandedNodes = this._getReachableExpandedNodes(this._data),
                expansionMetadata,
                length = reachableExpandedNodes.length,
                listenersHash = {},
                listenersIds = [],
                id,
                i;

            for (i = 0; i < length; i++) {
                expansionMetadata = this._expansionMap.get(reachableExpandedNodes[i]);
                if (expansionMetadata.listenerId) {
                    listenersIds.push(expansionMetadata.listenerId);
                } else {
                    listenersIds.push(
                        this._addListener(reachableExpandedNodes[i])
                    );
                }
                listenersHash[expansionMetadata.listenerId] = this._listenersHash[expansionMetadata.listenerId];
                delete this._listenersHash[expansionMetadata.listenerId];
            }
            for (id in this._listenersHash) {
                if (this._listenersHash.hasOwnProperty(id)) {
                    this._removeListener(id);
                }
            }
            this._listenersHash = listenersHash;
        }
    }

});