Source: widget/tourlist/View.js

(function() {
    /**
     * @typedef {Help4.control2.container.Container.Params} Help4.widget.tourlist.View.Params
     * @property {Help4.widget.tourlist.Widget} widget - the owner widget
     */

    /**
     * List view for tourlist widget.
     * @augments Help4.control2.container.Container
     * @property {Help4.widget.tourlist.Widget} __widget
     * @property {Function} _domRefreshExecutor
     * @property {Help4.jscore.MutualExclusion} _mutual
     */
    Help4.widget.tourlist.View = class extends Help4.control2.container.Container {
        /**
         * @override
         * @param {Help4.widget.tourlist.View.Params} params
         */
        constructor(params) {
            const {/** @type {Help4.widget.tourlist.Widget} */ widget} = params;

            Help4.widget.companionCore.Core.addStandardViewParameters(widget, params, 'contentDiv');

            const T = Help4.jscore.ControlBase.TYPES;
            super(params, {
                params: {
                    widget:  {type: T.instance, mandatory: true, private: true, readonly: true},
                    visible: {init: false},
                    role:    {init: 'list'}
                },
                statics: {
                    _domRefreshExecutor: {init: () => this.update(), destroy: false},
                    _mutual:             {init: new Help4.jscore.MutualExclusion()}
                },
                config: {
                    css: 'widget-tourlist-view'
                }
            });
        }

        /**
         * drop all unnecessary parameters from the tile control
         * and convert to a 1-level object for easier comparison
         * input is from {@link _updateStructuralCreate} or from {@link _updateStructuralExtract}
         * @param {Help4.widget.tourlist.TileParams|Object} data
         * @returns {Object}
         */
        static updateStructuralUnify(data) {
            const {projectId, catalogueKey, catalogueType, dataType} = data._metadata;
            return {projectId, catalogueKey, catalogueType, dataType};
        }

        /**
         * drop all unnecessary parameters from the tile control
         * and convert to a 1-level object for easier comparison
         * input is from {@link _updateStructuralCreate} or from {@link _updateStructuralExtract}
         * @param {Help4.widget.tourlist.TileParams|Object} data
         * @returns {Object}
         */
        static updateVisualUnify(data) {
            const {caption, hidden, status} = data;
            return {caption, hidden, status};
        }

        /**
         * update my view: change control properties
         * input is from {@link _updateStructuralCreate}
         * @param {Help4.widget.tourlist.View|Help4.widget.whatsnew.view.TileView} view
         * @param {Help4.widget.tourlist.TileParams} tile
         * @param {number} index
         */
        static updateVisualUpdate(view, {caption, hidden, status}, index) {
            // existing tiles and new tiles are in same order
            const control = view._store[index];
            if (!control) return;

            control.caption = caption;
            control.hidden = hidden;
            control.status = status;
        }

        /**
         * @override
         * @returns {Help4.widget.tourlist.View}
         */
        focus() {
            this.count() > 0
                ? this.get(0).focus()
                : this.focus();

            return this;
        }

        /**
         * focus handling - up/down arrow keys
         * @param {string} direction
         */
        focusListItem(direction) {
            let count = this.count();
            if (count > 0) {
                const visibleControls = [];
                this.forEach(control => control.visible && visibleControls.push(control));

                const focussedElement = Help4.widget.getActiveElement();
                let index = visibleControls.findIndex(control => control.getDom() === focussedElement);

                if (index >= 0) visibleControls[direction === 'down' ? ++index : --index]?.focus();
            }
        }

        /**
         * updates the tour list
         * @param {boolean} [isRefresh = true]
         * @returns {Promise<boolean>}
         */
        async update(isRefresh = true) {
            /** see {@link Help4.jscore.MutualExclusion} for more information */
            const {_mutual} = this;
            const mutualToken = _mutual.start();
            _mutual.access(mutualToken);  // immediately access; to avoid double condition evaluation in create()

            const {View} = Help4.widget.tourlist;

            return await Help4.widget.companionCore.Core.updateTileListView({
                isDestroyed: () => this.isDestroyed(),
                isRefresh,
                structural: {
                    create: () => this._updateStructuralCreate(mutualToken),
                    extract: () => this._updateStructuralExtract(),
                    unify: data => View.updateStructuralUnify(data),
                    update: {mutual: _mutual, mutualToken, container: this}
                },
                visual: {
                    unify: data => View.updateVisualUnify(data),
                    update: (tile, index) => View.updateVisualUpdate(this, tile, index)
                }
            });
        }

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

            const {_domRefreshExecutor, __widget} = this;
            Help4.widget.companionCore.Core.disconnectDomRefresh(_domRefreshExecutor, __widget.getContext());
        }

        /**
         * @override
         * @param {HTMLElement} dom
         * @returns {Promise<void>}
         */
        async _onDomCreated(dom) {
            const {/** @type {Help4.widget.tourlist.Widget} */ __widget} = this;

            // do not fill list container immediately as system is still booting and might not be ready
            // especially for condition element evaluation; wait a bit to let the boot finish
            await Help4.widget.companionCore.Core.waitControllerPlaybackServiceReady(__widget, 100);

            if (!this.isDestroyed()) {
                // observe tile clicks for tour starts
                this.addListener(['click', 'enter', 'space'], async ({target: [container, tile] = []}) => {
                    const {
                        /** @type {string} */ projectId,
                        /** @type {Help4.widget.help.CatalogueKeys} */ catalogueKey,
                        /** @type {Help4.widget.help.CatalogueTypes} */ catalogueType,
                        /** @type {Help4.widget.help.DataTypes} */ dataType
                    } = tile.getMetadata('projectId', 'catalogueKey', 'catalogueType', 'dataType');

                    this._fireEvent({type: 'startTour', tour: {projectId, catalogueKey, catalogueType, dataType}});
                });

                // add tiles
                await this.update(false);

                // observe dom refresh
                const context = __widget.getContext();
                Help4.widget.companionCore.Core.observeDomRefresh(this._domRefreshExecutor, context, this);
            }
        }

        /**
         * create new tile information
         * @protected
         * @param {number} mutualToken
         * @returns {Promise<?Help4.widget.tourlist.TileParams[]>}
         */
        async _updateStructuralCreate(mutualToken) {
            const {Tour} = Help4.widget.companionCore.data;
            const {_mutual} = this;

            const sortedProjects = await Tour.getFilteredTourProjects();
            if (this.isDestroyed() || !_mutual.access(mutualToken)) return null;

            return Tour.tourProjectsToTileParams(sortedProjects);
        }

        /**
         * extract current tile information
         * @protected
         * @returns {Object[]}
         */
        _updateStructuralExtract() {
            return this.map(control => control.dataFunctions.toObject());
        }
    }
})();