Source: control2/container/PanelWidgetContainer.js

(function() {
    /**
     * @typedef {Help4.control2.container.Container.Params} Help4.control2.container.PanelWidgetContainer.Params
     * @property {Object} panel - the panel control where this container lives in
     */

    /**
     * A container for widget tiles.
     * @augments Help4.control2.container.Container
     */
    Help4.control2.container.PanelWidgetContainer = class extends Help4.control2.container.SortedContainer {
        /**
         * @override
         * @param {Help4.control2.container.PanelWidgetContainer.Params} [params]
         */
        constructor(params) {
            const panel = params.panel;
            if (!panel) throw new Error('panel parameter is required!');

            const T = Help4.jscore.ControlBase.TYPES;
            super(params, {
                params: {
                    type:      {init: 'button.Button'},
                    panel:     {type: T.instance, init: panel, mandatory: true, private: true}
                },
                statics: {
                    _widgetEngineObserver: {}
                },
                config: {
                    css: 'panel-widgets'
                }
            });
        }

        /**
         * @override
         * @returns {Help4.control2.container.PanelWidgetContainer}
         */
        clean() {
            const current = _getCurrentWidgetIds.call(this);
            for (/** @type {Help4.widget.Widget} */ const widgetInstance of Help4.widget) {
                const {id} = widgetInstance.getDescriptor();
                current.indexOf(id) >= 0 && widgetInstance.registerPanel(null);  // unregister panel
            }
            return super.clean();
        }

        /** @override */
        _onReady() {
            super._onReady();

            this.addListener('click', event => {
                const id = event.target[1]?.getMetadata('id');  // target: [<container>, <button>]
                id && this._fireEvent({type: 'widget', id});
            });

            _startWidgetObservation.call(this);
        }
    }

    /**
     * @memberof Help4.control2.container.PanelWidgetContainer#
     * @private
     */
    function _startWidgetObservation() {
        const {TYPES} = Help4.EventBus;
        const eventBus = Help4.getController().getService('eventBus');

        // get widgets that already run on startup of panel
        for (/** @type {Help4.widget.Widget} */ const instance of Help4.widget) {
            _updateWidgets.call(this, {type: TYPES.widgetStart, engine: instance, descriptor: instance.getDescriptor()});
        }

        // observe changes to mode engines
        this._widgetEngineObserver = new Help4.observer.EventBusObserver(event => void _updateWidgets.call(this, event))
        .observe(eventBus, {type: [TYPES.widgetStart, TYPES.widgetVisibility]});
    }

    /**
     * @memberof Help4.control2.container.PanelWidgetContainer#
     * @param {{type: string, engine: Help4.widget.Widget, descriptor: ?Object}} eventBusEvent
     * @private
     */
    function _updateWidgets({type, engine: widgetInstance, descriptor: widgetDescriptor} = {}) {
        const {TYPES} = Help4.EventBus;

        switch (type) {
            case TYPES.widgetStart:
            case TYPES.widgetVisibility:
                widgetInstance.isStarted() && widgetInstance.isVisible()
                    ? _addWidget.call(this, widgetInstance)
                    : _removeWidget.call(this, widgetInstance, widgetDescriptor);
                break;
        }
    }

    /**
     * @memberof Help4.control2.container.PanelWidgetContainer#
     * @returns {string[]}
     * @private
     */
    function _getCurrentWidgetIds() {
        // get current widget IDs
        const ids = [];
        /** @type {Help4.control2.button.Button} */
        for (const widgetButton of this) {
            ids.push(widgetButton.getMetadata('id'));
        }
        return ids.sort();
    }

    /**
     * @memberof Help4.control2.container.PanelWidgetContainer#
     * @param {Help4.widget.Widget} widgetInstance
     * @private
     */
    function _addWidget(widgetInstance) {
        widgetInstance.registerPanel(this.__panel);
        _updateItemOrder.call(this);

        const {id, tile} = /** @type {Help4.widget.Widget.Descriptor} */ widgetInstance.getDescriptor();
        if (!tile) return;  // some widgets have no tile, e.g. Help4.widget.tour

        const {text, title, icon, color: css, visible = true} = tile;
        if (this.get({byMetadata: {id}})) return;  // already within panel

        const {widget: appearance} = Help4.control2.button.APPEARANCES;
        const role = 'listitem';
        this.addSorted({_metadata: {id}, _order: id, text, title, icon, css, appearance, visible, role});
    }

    /**
     * @memberof Help4.control2.container.PanelWidgetContainer#
     * @param {Help4.widget.Widget} widgetInstance
     * @param {Object} widgetDescriptor
     * @private
     */
    function _removeWidget(widgetInstance, widgetDescriptor) {
        const {id} = widgetInstance?.getDescriptor() || widgetDescriptor || {};
        id && this.remove({byMetadata: {id}});
        _updateItemOrder.call(this);
    }

    /**
     * @memberof Help4.control2.container.PanelWidgetContainer#
     * @private
     */
    function _updateItemOrder() {
        const orderMap = {};
        Help4.widget.forEach((/** @type {Help4.widget.Widget} */ widget) => {
            const {id, tile: {position = -1} = {}} = widget.getDescriptor();
            orderMap[position] ||= [];
            orderMap[position].push(id);
        });

        const orderList = [];
        const positions = Object.keys(orderMap).map(id => Number(id)).sort();
        for (const position of positions) {
            position >= 0 && orderList.push(...orderMap[position]);  // skip unsorted ones
        }

        orderMap[-1] && orderList.push(...orderMap[-1]);  // add unsorted ones to the end

        this.itemOrder = orderList;
    }
})();