Source: widget/help/view2/TileContainer.js

(function() {
    /**
     * @typedef {Object} Help4.widget.help.view2.TileContainer.Params
     * @property {Help4.widget.help.Widget} widget
     * @property {Help4.widget.help.view2.View} view
     * @property {Help4.control2.container.Container} contentView
     * @property {Help4.control2.container.Container} tileView
     * @property {boolean} stealth
     * @property {Help4.widget.help.view2.Tile.Descriptor} [selected]
     * @property {Help4.widget.help.view2.Tile.Descriptor} [hovered]
     * @property {Help4.widget.help.view2.Tile.Descriptor} [wmHovered]
     */

    /**
     * This can be seen as of type: Help4.widget.help.view2.Tile[]
     * @augments Help4.jscore.ControlBase
     * @property {Help4.widget.help.Widget} __widget
     * @property {Help4.widget.help.view2.View} __view
     * @property {Help4.control2.container.Container} __contentView
     * @property {Help4.control2.container.Container} __tileView
     * @property {Help4.widget.help.view2.Tile.Descriptor|null} selected
     * @property {Help4.widget.help.view2.Tile.Descriptor|null} hovered
     * @property {Help4.widget.help.view2.Tile.Descriptor|null} wmHovered
     * @property {Help4.widget.help.view2.Tile.Descriptor[]} callouts
     * @property {?Help4.widget.help.view2.Tile.Descriptor} callout
     * @property {Help4.widget.help.view2.Tile.Descriptor[]} announcements
     * @property {Help4.widget.help.view2.Tile.Descriptor|null|false} announcement - string: currently shown; null: not yet shown; false: do not show again
     * @property {?Help4.widget.help.view2.Tile.Descriptor} quickTour
     * @property {boolean} stealth
     * @property {Map<string, Help4.widget.help.view2.Tile>} _tilesMap
     * @property {Help4.observer.EventBusObserver} _eventBusObserver
     */
    Help4.widget.help.view2.TileContainer = class extends Help4.jscore.ControlBase {
        /**
         * @constructor
         * @param {Help4.widget.help.view2.TileContainer.Params} params
         */
        constructor(params) {
            const {TYPES: T} = Help4.jscore.ControlBase;
            super(params, {
                params: {
                    widget:        {type: T.instance, mandatory: true, readonly: true, private: true},
                    view:          {type: T.instance, mandatory: true, readonly: true, private: true},
                    contentView:   {type: T.instance, mandatory: true, readonly: true, private: true},
                    tileView:      {type: T.instance, mandatory: true, readonly: true, private: true},
                    selected:      {type: T.string_null},
                    hovered:       {type: T.string_null},
                    wmHovered:     {type: T.string_null},
                    callouts:      {type: T.array},  // all unseen callouts
                    callout:       {type: T.string_null},  // current callout
                    announcements: {type: T.array},  // all unseen announcements
                    announcement:  {type: T.atomic},  // current announcement
                    quickTour:     {type: T.string_null},  // current QuickTour
                    stealth:       {type: T.boolean},  // stealth mode, announcements only
                },
                statics: {
                    _tilesMap:         {init: new Map(), destroy: false},
                    _eventBusObserver: {init: new Help4.observer.EventBusObserver(({hotkey}) => _onHotkeyEvent.call(this, hotkey))}
                },
                config: {
                    onPropertyChange: ({name, value, oldValue}) => {
                        switch (name) {
                            case 'selected':
                                _select.call(this, value, oldValue);
                                break;
                            case 'hovered':
                                _hover.call(this, value, oldValue);
                                break;
                            case 'wmHovered':
                                _wmHover.call(this, value, oldValue);
                                break;
                            case 'quickTour':
                                value && _showQuickTour.call(this, value);
                                break;
                            case 'stealth':
                                this._tilesMap.forEach(tile => tile.stealth = value);
                                break;
                        }
                    }
                }
            });

            const {eventBus} = this.__widget.getContext();
            this._eventBusObserver.observe(eventBus, {type: Help4.EventBus.TYPES.hotkey});
        }

        /** @override */
        destroy() {
            this._tilesMap.forEach(tile => tile.destroy());
            super.destroy();
        }

        /** @param {?Help4.widget.help.view2.Tile.Descriptor} [callout] */
        showCallout(callout = this.callout) {
            const {callouts, announcement, _tilesMap, quickTour, stealth} = this;
            if (announcement || quickTour || stealth) return;  // do not interfere with announcements

            const hotspotOk = descriptor => {
                const tile = _tilesMap.get(descriptor);
                return tile
                    ? !!tile.hotspotControlId || !tile.hasHotspot()
                    : false;
            }

            const selectCallout = () => {
                const {callout: descriptor} = this;
                this.selected = descriptor;
                this._fireEvent({type: 'showCallout', descriptor});
            }

            if (callouts.length) {
                if (callout) {
                    this.callout = callout;
                    if (hotspotOk(callout)) selectCallout();  // only select callout if hotspot is visible
                } else {
                    // next callout; FIFO but use the first one that has no or a visible hotspot
                    for (const c of callouts) {
                        if (hotspotOk(c)) {
                            this.callout = c;
                            selectCallout();
                            return;
                        }
                    }

                    // no callout is visible; use first one but do not select it
                    this.callout = callouts[0];
                }
            } else {
                this.callout = null;
            }
        }

        /** show announcement */
        showAnnouncement() {
            // only show announcement if not already shown during this session
            const {announcements, announcement, _tilesMap, quickTour} = this;
            if (quickTour) return;

            if (announcement === null && announcements.length) {
                const {
                    COOKIE_KEYS: {ANNOUNCEMENT: type},
                    widget: {help: {Cookie}}
                } = Help4;

                const [a] = announcements;
                const {projectTile: {splashOption, id: key}} = _tilesMap.get(a);

                const {__widget} = this;
                splashOption === 'always'
                    ? Cookie.setPageCookie(__widget, {type, key, session: true})
                    : Cookie.setPageCookie(__widget, {type, key});

                this.announcement = a;
                this.selected = a;
            }
        }

        /**
         * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
         * @returns {Help4.widget.help.ProjectTile}
         */
        get(descriptor) {
            return this._tilesMap.get(descriptor)?.projectTile;
        }

        /**
         * @param {Object} params
         * @param {Help4.widget.help.ProjectTile[]} params.projectTiles
         * @param {Object} params.hotspotStatus
         * @param {boolean} [params.stealth]
         */
        set({projectTiles, hotspotStatus, stealth}) {
            const {
                COOKIE_KEYS: {ANNOUNCEMENT},
                widget: {help}
            } = Help4;
            const {view2} = help;

            const {__widget, __view, __contentView: contentView, __tileView: tileView, _tilesMap} = this;
            const {whatsnew} = __view;

            stealth ??= this.stealth;

            // assemble the new projectTiles in correct order
            // do not directly override _tilesMap as it is needed to check for to-be-updated or to-be-removed tiles
            const map = /** @type {Map<string, Help4.widget.help.view2.Tile>} */ new Map();

            // init map; needed for remove
            projectTiles.forEach(projectTile => {
                const descriptor = view2.extractTileDescriptor(projectTile);
                map.set(descriptor, null);
            });

            const removed = new Set();
            const added = new Set();
            const updated = new Set();

            // remove no longer existing projectTiles
            // removing first is much better for adding and updating below!
            // do NOT use forEach here; as new Map().entries().forEach is not supported in FF and Safari!
            for (const descriptor of _tilesMap.keys()) {
                if (!map.has(descriptor)) {
                    const tile = _tilesMap.get(descriptor);
                    tile.destroy();
                    removed.add(descriptor);
                }
            }

            const widgetActive = __widget.isActive();

            // update existing and add new projectTiles
            projectTiles.forEach((projectTile, index) => {
                const descriptor = view2.extractTileDescriptor(projectTile);
                let tile = /** @type {?Help4.widget.help.view2.Tile} */ _tilesMap.get(descriptor);

                if (tile) {
                    // update tile
                    // PERFORMANCE: do not use single assignments
                    const modified = tile.update({
                        projectTile,
                        widgetActive,
                        hotspotStatus: hotspotStatus[projectTile.id],
                        tilePosition: index,
                        stealth
                    });

                    modified && updated.add(descriptor);
                } else {
                    // create new tile
                    added.add(descriptor);

                    tile = new view2.Tile({
                        widget: __widget,
                        view: __view,
                        container: this,
                        contentView,
                        tileView,
                        projectTile,
                        whatsnew,
                        tilePosition: index,
                        widgetActive,
                        hotspotStatus: hotspotStatus[projectTile.id],
                        stealth
                    })
                    .addListener(['closeLightbox', 'closeBubble', 'hotspotHidden'], ({descriptor}) => {
                        if (this.selected === descriptor) this.selected = null;
                    })  // deselect after lightbox or bubble close; or after hotspot has become invisible
                    .addListener('hotspotVisible', ({descriptor}) => {
                        // hotspot becomes visible again; XRAY-6170
                        // take care to not override an existing selection: XRAY-6436
                        if (!this.selected && this.callout === descriptor) {
                            this.selected = descriptor;
                        }
                    })
                    .addListener('userCloseBubble', ({descriptor}) => this.callout === descriptor && this._fireEvent({type: 'closeCallout', descriptor}))  // stop callout handling
                    .addListener('userCloseLightbox', ({descriptor, checkbox}) => {
                        if (this.quickTour === descriptor) {
                            // stop QuickTour handling
                            this.quickTour = null;
                        } else if (this.announcement === descriptor) {
                            // stop announcement handling
                            this._fireEvent({type: 'closeAnnouncement', descriptor, checkbox});
                        }
                    })
                    .addListener('lightboxCheckbox', ({descriptor, active}) => {  // announcement checkbox handling
                        const {announcement, _tilesMap} = this;
                        if (announcement !== descriptor) return;

                        const {projectTile: {splashOption, id}} = _tilesMap.get(descriptor);
                        if (splashOption === 'always') {
                            const {Cookie} = help;
                            active
                                ? Cookie.setPageCookie(__widget, {type: ANNOUNCEMENT, key: id})
                                : Cookie.remPageCookie(__widget, {type: ANNOUNCEMENT, key: id});
                        }
                    });
                }

                map.set(descriptor, tile);
            });

            this._tilesMap = map;
            this.stealth = stealth;

            if (removed.size || updated.size || added.size) {
                Help4.WM.listeners.onTileChange.onEvent({type: 'tile.change'});
            }

            if (this.selected && !map.has(this.selected)) {
                // XRAY-6436: selected element is gone, maybe due to a condition
                this.selected = null;  // deselect
            }
        }

        /**
         * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
         * @returns {boolean}
         */
        stopHotspotAnimation(descriptor) {
            const tile = /** @type {?Help4.widget.help.view2.Tile} */ this._tilesMap.get(descriptor);
            return tile?.stopHotspotAnimation() || false;
        }

        /**
         * is essentially the same as "this.hovered = false" but will add a wait time
         * @returns {Promise<boolean>}
         */
        async leave() {
            const {BUBBLE_CLOSE_TIME} = Help4.widget.help.view2.View;
            const {_tilesMap, hovered} = this;

            const tile = /** @type {?Help4.widget.help.view2.Tile} */ hovered && _tilesMap.get(hovered);
            const {descriptor, success = false} = await tile?.hover(false, BUBBLE_CLOSE_TIME) || {};
            if (this.isDestroyed()) return false;

            if (success && hovered === descriptor) {
                this.hovered = null;
                return true;
            }
            return false;
        }
    }

    /**
     * @memberof Help4.widget.help.view2.TileContainer#
     * @private
     * @param {?Help4.widget.help.view2.Tile.Descriptor} value
     * @param {?Help4.widget.help.view2.Tile.Descriptor} oldValue
     */
    function _select(value, oldValue) {
        const {_tilesMap, quickTour} = this;

        // deselect previous tile
        let tile = /** @type {?Help4.widget.help.view2.Tile} */ oldValue && _tilesMap.get(oldValue);
        if (tile) {
            Help4.WM.listeners.onHotspotEvent.onEvent({type: 'deselect', descriptor: oldValue});
            tile.select(false);
        }

        // select new tile
        tile = /** @type {?Help4.widget.help.view2.Tile} */ value && _tilesMap.get(value);
        const selected = tile?.select(true);
        if (selected) {
            Help4.WM.listeners.onHotspotEvent.onEvent({type: 'select', descriptor: value});
        }
        if (!selected && !quickTour) {
            // tile refused to select itself; e.g. a link tile that opens the content in a new window
            this.selected = null;
        }
    }

    /**
     * @memberof Help4.widget.help.view2.TileContainer#
     * @private
     * @param {?Help4.widget.help.view2.Tile.Descriptor} value
     * @param {?Help4.widget.help.view2.Tile.Descriptor} oldValue
     */
    function _hover(value, oldValue) {
        const {_tilesMap} = this;

        // leave previous tile
        let tile = /** @type {?Help4.widget.help.view2.Tile} */ oldValue && _tilesMap.get(oldValue);
        if (tile) {
            Help4.WM.listeners.onHotspotEvent.onEvent({type: 'leave', descriptor: oldValue});
            tile.hover(false);
        }

        // hover new tile
        tile = /** @type {?Help4.widget.help.view2.Tile} */ value && _tilesMap.get(value);
        if (tile) {
            Help4.WM.listeners.onHotspotEvent.onEvent({type: 'hover', descriptor: value});
            tile.hover(true);
        }
    }

    /**
     * @memberof Help4.widget.help.view2.TileContainer#
     * @private
     * @param {?Help4.widget.help.view2.Tile.Descriptor} value
     * @param {?Help4.widget.help.view2.Tile.Descriptor} oldValue
     */
    function _wmHover(value, oldValue) {
        const {_tilesMap} = this;

        // leave previous tile
        let tile = /** @type {?Help4.widget.help.view2.Tile} */ oldValue && _tilesMap.get(oldValue);
        tile?.wmHover(false);

        // hover new tile
        tile = /** @type {?Help4.widget.help.view2.Tile} */ value && _tilesMap.get(value);
        tile?.wmHover(true);
    }

    /**
     * @memberof Help4.widget.help.view2.TileContainer#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor} value
     */
    function _showQuickTour(value) {
        // QuickTour will open at the beginning; maybe with some short delay

        // in case an announcement has opened in the meantime:
        // the user will not have actively seen it
        // therefore reset the whole announcement status
        const {announcement, _tilesMap} = this;
        if (announcement) {
            const {
                COOKIE_KEYS: {ANNOUNCEMENT: type},
                widget: {help: {Cookie}}
            } = Help4;

            // cookie removal is okay - otherwise announcement would not have opened
            // QuickTour will open so fast that user would not have been able to click the checkbox
            const {__widget} = this;
            const {projectTile: {id: key}} = _tilesMap.get(announcement);
            Cookie.remPageCookie(__widget, {type, key, session: true});
            Cookie.remPageCookie(__widget, {type, key});

            // reset announcement status
            this.announcement = null;
        }

        // reset callout status
        this.callout = null;

        // show QuickTour
        this.selected = value;
    }

    /**
     * @memberof Help4.widget.help.view2.TileContainer#
     * @private
     * @param {string} hotkey
     */
    function _onHotkeyEvent(hotkey) {
        if (hotkey !== Help4.HOTKEY.escape) return;

        const {TileBubble, TileLightbox} = Help4.widget.help.view2;
        const {_tilesMap, selected} = this;
        const tile = _tilesMap.get(selected);

        if (tile?.bubbleControlId) {
            TileBubble.closeBubble(tile);
        } else if (tile?.lightboxControlId) {
            TileLightbox.closeLightbox(tile);
        }
    }
})();