Source: control2/container/Container.js

(function() {
    /**
     * @namespace container
     * @memberof Help4.control2
     */
    Help4.control2.container = {};

    /**
     * @typedef {Help4.control2.Control.Params} Help4.control2.container.Container.Params
     * @property {string} [type = 'Control'] - default type of the hosted controls
     * @property {any[]} [data = []] - to be created controls on container creation
     * @property {?Help4.control2.AreaXYWH} [virtualArea = null] - virtual DOM area that the container should see as its space (containers will always have size 0x0 - but can use virtual space)
     */

    /**
     * A container to structure some controls. Will also provide a container element for those controls within the DOM.
     * @augments Help4.control2.Control
     * @property {string} type - default type of the hosted controls
     * @property {any[]} data - to be created controls on container creation
     * @property {?Help4.control2.AreaXYWH} virtualArea - virtual DOM area that the container should see as its space (containers will always have size 0x0 - but can use virtual space)
     */
    Help4.control2.container.Container = class extends Help4.control2.Control {
        /**
         * @override
         * @param {Help4.control2.container.Container.Params} [params]
         * @param {Help4.jscore.ControlBase.Params} [derived]
         */
        constructor(params, derived) {
            const T = Help4.jscore.ControlBase.TYPES;
            super(params, {
                params: {
                    type:        {type: T.string, init: 'Control', readonly: true},
                    data:        {type: T.array, private: true, readonly: true},
                    virtualArea: {type: T.xywh_null}
                },
                statics: {
                    _store: {init: [], destroy: false}
                },
                config: {
                    css: 'control-container'
                },
                derived
            });
        }

        /** @override */
        _onBeforeDestroy() {
            this.clean();
        }

        /**
         * @override
         * @param {HTMLElement} dom - control DOM
         */
        _onDomCreated(dom) {
            const {__data} = this;
            if (__data.length) this.add(...__data);
        }

        /** @returns {Help4.control2.container.Container} */
        clean() {
            const {_store} = this;

            // control.destroy() will invoke this.remove(<index>)
            // and modify _store outside of this loop
            // therefore DO NOT use an index based loop here!
            let control;
            while (control = _store[0]) {
                control.destroy();
            }

            this._store = [];
            return this;
        }

        /**
         * create controls out of parameters and add them to this container
         * @param {Help4.control2.Control.Params} params - parameters for the to-be-created controls
         * @returns {?Help4.control2.Control|Array<?Help4.control2.Control>} the added control(s)
         */
        add(...params) {
            const toAdd = params.length;
            const added = new Array(toAdd);
            for (const [index, item] of Help4.arrayEntries(params)) {
                added[index] = _addFromObject.call(this, item);
            }
            return toAdd === 1 ? added[0] : added;
        }

        /**
         * replaces a control at the given position
         * @param {Help4.control2.Control.Params} params - parameters for the to-be-created controls
         * @param {string} controlId - the control id of the to-be-replaced control
         * @returns {?Help4.control2.Control} the added control
         */
        replace(params, controlId) {
            const index = this.getIndex(controlId);
            if (index < 0) return null;

            const control = this.insertAt(params, index);
            this.remove(controlId);
            return control;
        }

        /**
         * moves a control to a certain position
         * @param {string} controlId - the control id of the to-be-moved control
         * @param {number} index - new position
         */
        moveTo(controlId, index) {
            const {/** @type {Help4.control2.Control[]} */ _store} = this;

            const control = this.get(controlId);
            const current = control && this.getIndex(controlId);
            if (!control || current === index) return;

            const dom = this.getDom();
            const controlDom = control.getDom();

            if (index >= _store.length) {
                // move to end
                dom.appendChild(controlDom);
                _store.splice(current, 1);
                _store.push(control);
            } else {
                const successor = _store[index];
                const successorDom = successor.getDom();
                dom.insertBefore(controlDom, successorDom);
                _store.splice(current, 1);
                _store.splice(index, 0, control);
            }
        }

        /**
         * inserts a control at the given position
         * @param {Help4.control2.Control.Params} params - parameters for the to-be-created controls
         * @param {number} index - the insert position
         * @returns {?Help4.control2.Control} the added control
         */
        insertAt(params, index) {
            const {/** @type {Help4.control2.Control[]} */ _store} = this;

            // simple: insert at end
            if (index >= _store.length) return this.add(params);

            // complex: insert before another control

            // insert control as usual
            const control = /** @type {?Help4.control2.Control} */ this.add(params);
            if (control) {
                // get control at target position (the to-be-successor)
                const successor = /** @type {Help4.control2.Control} */ _store[index];

                // get DOM nodes and move new control from end to target position
                const dom = this.getDom();
                const successorDom = successor.getDom();
                const controlDom = control.getDom();
                dom.insertBefore(controlDom, successorDom);

                // adopt internal _store: move new control from end to target position
                _store.splice(index, 0, _store.pop());
                return control;
            }

            return null;
        }

        /**
         * is able to find a control also within sub containers
         * @param {string} controlId
         * @returns {Array<Help4.control2.Control, number, Help4.control2.container.Container>|Array<null,null,null>}
         */
        find(controlId) {
            const {Container} = Help4.control2.container;

            for (const [index, control] of this.entries()) {
                if (control.id === controlId) {
                    return [control, index, this];
                } else if (control instanceof Container) {
                    const value = control.find(controlId);
                    if (value[0]) return value;
                }
            }
            return [null, null, null];
        }

        /**
         * @param {*} controlId - index of control in container, or control ID or metadata information
         * @returns {?Help4.control2.Control}
         */
        get(controlId) {
            if (typeof controlId === 'number') {  // controlId is index
                return this._store[controlId] || null;
            } else if (typeof controlId === 'string') {  // controlId is control id
                const [control] = this.find(controlId);
                return control;
            } else if (typeof controlId === 'object' && controlId) {
                // special get mode: e.g. {byMetadata: {id: '1'}}
                const k = Object.keys(controlId)[0];
                if (k === 'byMetadata') {
                    const [control] = _getByMetadata.call(this, controlId[k]);
                    return control;
                }
            }
            return null;
        }

        /**
         * @param {*} controlId - index of control in container, or control ID or metadata information
         * @returns {Help4.control2.Control|null} the removed control
         */
        remove(controlId) {
            if (typeof controlId === 'number') {  // controlId is an index
                const {_store} = this;
                const control = _store[controlId];
                if (control) {
                    _store.splice(controlId, 1);
                    control.destroy();
                    return control;
                }
            } else if (typeof controlId === 'string') {  // controlId is control id
                const [control, index, container] = this.find(controlId);
                if (control) return container.remove(index);
            } else if (typeof controlId === 'object' && controlId) {
                // special get mode: e.g. {byMetadata: {id: '1'}}
                const k = Object.keys(controlId)[0];
                if (k === 'byMetadata') {
                    const [control, index, container] = _getByMetadata.call(this, controlId[k]);
                    if (control) return container.remove(index);
                }
            }
            return null;
        }

        /**
         * @param {string} controlId
         * @returns {number}
         */
        getIndex(controlId) {
            for (const [index, control] of this.entries()) {
                if (control.id === controlId) return index;
            }
            return -1;
        }

        /** @returns {number} */
        count() {
            return this._store.length;
        }

        /** @returns {Help4.control2.AreaXYWH} */
        getArea() {
            const {virtualArea} = this;
            if (virtualArea) return virtualArea;

            let {x, y, w, h} = super.getArea();
            if (!x  && !y && !w && !h) {
                // all containers that have no defined area are considered "virtual"
                // and might contain controls all over the window
                // e.g. consider a container with only controls that are "position: absolute"
                w = window.innerWidth;
                h = window.innerHeight;
            }
            return {x, y, w, h};
        }

        /**
         * @param {Help4.typedef.MapExecutor} executor
         * @returns {Array<*>}
         */
        map(executor) {
            return this._store.map((control, index, store) => executor(control, index, store, this));
        }

        /**
         * iterates the container
         * @param {Help4.typedef.ForEachExecutor} executor
         */
        forEach(executor) {
            this._store.forEach((control, index, store) => executor(control, index, store, this));
        }

        /**
         * iterates the container
         * @param {Help4.typedef.EveryExecutor} executor
         * @returns {boolean}
         */
        every(executor) {
            return this._store.every((control, index, store) => executor(control, index, store, this));
        }

        /**
         * for (const [index, control] of container.entries()) {
         *     ...
         * }
         *
         * @generator
         * @yields [number, Help4.control2.Control]
         */
        *entries() {
            // do not cache _store.length as it might change during the loop
            const {_store} = this;
            let index = 0;
            while (_store[index]) {
                yield [index, _store[index]];
                index++;
            }
        }

        /**
         * for (const control of container) {
         *     ...
         * }
         *
         * @returns {{next: (function(): {value: *, done: boolean})}}
         */
        [Symbol.iterator]() {
            let index = -1;
            return {
                next: () => ({
                    value: this._store[++index],
                    done: !(index in this._store)
                })
            };
        }
    }

    /**
     * @memberof Help4.control2.container.Container#
     * @param {Object} metadata
     * @returns {Array<Help4.control2.Control, number, Help4.control2.container.Container>|Array<null, null, null>}
     * @private
     */
    function _getByMetadata(metadata) {
        const {Container} = Help4.control2.container;
        const keys = Object.keys(metadata);

        for (const [index, control] of this.entries()) {
            if (keys.every(key => control.getMetadata(key) === metadata[key])) {
                return [control, index, this];
            } else if (control instanceof Container) {
                const value = _getByMetadata.call(control, metadata);
                if (value[0]) return value;
            }
        }
        return [null, null, null];
    }

    /**
     * @memberof Help4.control2.container.Container#
     * @param {Object} object
     * @returns {?Help4.control2.Control}
     * @private
     */
    function _addFromObject(object) {
        if (object.id && this.get(object.id)) return null;  // duplicate check

        const {control2} = Help4;
        const {_store, type} = this;
        control2.deriveClassParams(this, object);
        object.container = this;

        const classObject = control2.getClass(object.controlType || type);
        if (classObject) {
            const control = /** @type {Help4.control2.Control} */ new classObject(object)
            .addListener('*', event => void this._fireEvent(event));

            _store.push(control);
            return control;
        }

        return null;
    }
})();