Source: ui/substitution.reel/substitution.js

/**
 * @module "montage/ui/substitution.reel"
 */
var Slot = require("../slot.reel").Slot,
    Promise = require("../../core/promise").Promise,
    logger = require("../../core/logger").logger("substitution");

/**
 * The substitution is a structural component and it should be used when there
 * are different types of content (e.g.: different panels) at the same time but
 * only one of them is shown at a time.
 *
 * A possible use case for the substitution is the implementation of a Tab
 * component content pane.
 *
 * The different types of content of a substitution are configured by declaring
 * them in the template as the DOM content of the substitution.
 * Each type of content is given to the substitution as a template argument.
 * Template arguments are declared by assigning the attribute `data-arg` to an
 * element that is the immediate child of the substitution.
 *
 * ##### Example - Declaring the substitution content
 * ```html
 * <div data-montage-id="substitution">
 *     <div data-arg="info" data-montage-id="infoPanel"></div>
 *     <div data-arg="contacts" data-montage-id="contactsPanel"></div>
 *     <div data-arg="review" data-montage-id="reviewPanel"></div>
 * </div>
 * ```
 * `info`, `contacts` and `review` are the types of content declared and
 * available as substitution content.
 * The type of content displayed by the substitution is defined by the
 * [switchValue]{@link Substitution#switchValue} property. The available values
 * of this property in this example are: `info`, `contacts` and `review`.
 *
 * ##### Example - Other substitution template configurations
 * ```html
 * <div data-montage-id="substitution">
 *     <div data-arg="info">
 *         Name: John Doe
 *     </div>
 *     <div data-arg="contacts">
 *         E-mail: ...
 *         Mobile: ...
 *     </div>
 *     <div data-arg="review">
 *         ...
 *     </div>
 * </div>
 * ```
 *
 * @class Substitution
 * @classdesc A structural component that reveals one of its template arguments
 * at a time.
 * @extends Slot
 */
exports.Substitution = Slot.specialize( /** @lends Substitution.prototype # */ {

    hasTemplate: {
        enumerable: false,
        value: false
    },

    constructor: {
        value: function Substitution() {
            this._switchElements = Object.create(null);
            this._switchComponents = Object.create(null);
            this._switchComponentTreeLoaded = Object.create(null);
        }
    },

    _allChildComponents: {
        value: null
    },

    deserializedFromTemplate: {
        value: function () {
            this._allChildComponents = this.childComponents.slice(0);

            if (this.switchValue) {
                this._loadSwitchComponentTree(this.switchValue);
            }
        }
    },

    _switchElements: {
        value: null
    },
    _switchComponentTreeLoaded: {
        value: null
    },

    /**
     * This method is used to dynamically add content to the substitution. This
     * is usually done by declaring the content in the template as the DOM
     * content of the substitution. However, in more advanced usages of the
     * substitution, this information might not be available at writing time.
     *
     * Throws when the `element` given has a parent node.
     *
     * @function
     * @param {string} key The key that identifies the content given, similar to
     *                 `data-arg` when declaring the content in the template.
     * @param {Node} element The element that will be shown when the `key` is
     *               the selected [switchValue]{@link Substitution#switchValue}.
     *               This element needs to be detached from the DOM and cannot
     *               have a parent node.
     */
    addSwitchElement: {
        value: function (key, element) {
            if (element.parentNode) {
                throw new Error("Can't handle elements inside the DOM.");
            }

            this._switchElements[key] = element;
            this._switchComponents[key] = element.component;
            this._findFringeComponents(element, this._allChildComponents);
        }
    },

    _findFringeComponents: {
        value: function (element, components) {
            var nodes;

            components = components || [];

            if (element.component) {
                components.push(element.component);
            } else {
                nodes = element.children;
                for (var i = 0, node; (node = nodes[i]); i++) {
                    this._findFringeComponents(node, components);
                }
            }

            return components;
        }
    },

    _drawnSwitchValue: {
        value: null
    },

    _switchValue: {
        value: null
    },

    /**
     * The switch value selects which content the substitution should show.
     * The possible values are the ones defined as template arguments of the
     * substitution.
     *
     * @type {string}
     */
    switchValue: {
        get: function () {
            return this._switchValue;
        },
        set: function (value) {

            if (this._switchValue === value || this._isSwitchingContent) {
                return;
            }

            this._switchValue = value;

            // switchElements is only ready after the first draw
            // At first draw the substitution automatically draws what is in
            // the switchValue so we defer any content loading until the first
            // draw.
            if (!this._firstDraw && !this.isDeserializing) {
                this._loadContent(value);
            }
        }
    },

    enterDocument: {
        value: function (firstTime) {
            var argumentNames;

            Slot.prototype.enterDocument.apply(this, arguments);

            if (firstTime) {
                argumentNames = this.getDomArgumentNames();
                for (var i = 0, name; (name = argumentNames[i]); i++) {
                    this._switchElements[name] = this.extractDomArgument(name);
                    this._switchComponents[name] = this._switchElements[name].component;
                }

                this._loadContent(this.switchValue);
                // TODO: Force the component to update its DOM now because the
                // updateComponentDom already happened for this draw cycle.
                // In the future the DrawManager will handle adding and
                // removing nodes from the DOM at any time before draw().
                this._updateComponentDom();
            }
        }
    },

    _loadContent: {
        value: function (value) {
            var typeOfValue = typeof value;
            this.content = this._switchElements[value] || null;

            if ((typeOfValue === "string" || typeOfValue === "number") && !this._switchComponentTreeLoaded[value]) {
                this._loadSwitchComponentTree(value);
            }
        }
    },

    contentDidChange: {
        value: function (newContent, oldContent) {
            Slot.prototype.contentDidChange.call(this, newContent, oldContent);

            if (this._drawnSwitchValue) {
                if (this._switchComponents[this._drawnSwitchValue]) {
                    this._switchElements[this._drawnSwitchValue] = this._switchComponents[this._drawnSwitchValue].element;
                }
            }
            this._drawnSwitchValue = this._switchValue;
        }
    },

    _loadSwitchComponentTree: {
        value: function (value) {
            var self = this,
                childComponents = this._allChildComponents,
                element = this._switchElements[value],
                substitutionElement = this.element,
                canDrawGate = this.canDrawGate,
                component,
                currentElement,
                promises = [];

            if (!element) {
                element = this._getSubstitutionDomArgument(value);
            }

            for (var i = 0; i < childComponents.length; i++) {
                component = childComponents[i];
                currentElement = component.element;

                // Search the DOM tree up until we find the switch element or
                // the substitution element
                while (currentElement !== element &&
                       currentElement !== substitutionElement &&
                       currentElement.parentNode) {
                    currentElement = currentElement.parentNode;
                }
                // If we found the switch element before finding the
                // substitution element it means this component is inside the
                // selected switch value.
                if (currentElement === element) {
                    promises.push(component.loadComponentTree());
                }
            }

            if (promises.length > 0) {
                canDrawGate.setField(value + "ComponentTreeLoaded", false);

                Promise.all(promises).then(function () {
                    self._switchComponentTreeLoaded[value] = true;
                    canDrawGate.setField(value + "ComponentTreeLoaded", true);
                    self._canDraw = true;
                    self.needsDraw = true;
                });
            } else {
                this._switchComponentTreeLoaded[value] = true;
                this.needsDraw = true;
            }
        }
    },

   /**
    * This function is used to get the dom arguments before the first draw,
    * _domArguments are only available at the first draw.
    * We need it before so we can start loading the component tree as soon as
    * possible without having to wait for the first draw.
    * @private
    */
    _getSubstitutionDomArgument: {
        value: function (name) {
            var candidates,
                node,
                element,
                elementId,
                serialization,
                labels,
                template = this._ownerDocumentPart.template;

            element = this.element;
            candidates = element.querySelectorAll("*[" + this.DOM_ARG_ATTRIBUTE + "='" + name + "']");

            // Make sure that the argument we find is indeed part of element and
            // not an argument from an inner component.
            nextCandidate:
            for (var i = 0, candidate; (candidate = candidates[i]); i++) {
                node = candidate;
                while ((node = node.parentNode) !== element) {
                    elementId = template.getElementId(node);

                    // Check if this node is an element of a component.
                    // TODO: Make this operation faster
                    if (elementId) {
                        serialization = template.getSerialization();
                        labels = serialization.getSerializationLabelsWithElements(
                            elementId);

                        if (labels.length > 0) {
                            // This candidate is inside another component so
                            // skip it.
                            continue nextCandidate;
                        }
                    }
                }
                return candidate;
            }
        }
    },

    /**
     * By default the substitution doesn't expand the entire component tree of
     * all its content, only of the content that needs to be shown.
     * This is an optimization to avoid loading all the content at page load
     * time.
     *
     * However, if for some reason it is desirable to load the entire content
     * at page load time this property can be set to `true`.
     *
     * @type {boolean}
     * @default false
     */
    shouldLoadComponentTree: {
        value: false
    },

    transition: {
        value: null
    }
});