Source: widget/help/view2/View.js

(function() {
    /**
     * @typedef {Object} Help4.widget.help.view2.View.Params
     * @property {Help4.widget.help.Widget} widget
     * @property {boolean} stealth
     */

    /**
     * @typedef {Object} Help4.widget.help.view2.View.Cache
     * @property {string} [screenId]
     * @property {Help4.widget.help.CatalogueKeys} [catalogueKey]
     * @property {string[]} [helpIds]
     * @property {Help4.widget.help.ProjectTile[]} [tiles]
     * @property {Help4.widget.help.ProjectTile[]} [conditionTiles]
     * @property {boolean} [isDataUpdate]
     * @property {Object} [persist] - information that needs to persist and be (de)serialized
     * @property {Object} [persist.animation] - already seen animations; see {@link Help4.widget.help.TileDescriptor}
     * @property {Object} [persist.callout] - already shown callouts; see {@link Help4.widget.help.TileDescriptor}
     * @property {Object} [persist.announcement_always] - already shown announcements; see {@link Help4.widget.help.TileDescriptor}
     * @property {Object} [persist.announcement_once] - already shown announcements; see {@link Help4.widget.help.TileDescriptor}
     * @property {string} [select]
     * @property {boolean} [stealth]
     */

    /**
     * @typedef {'activate'|'deactivate'|'playbackActive'|'controllerOpen'|'controllerClose'|'widgetDeactivate'|'navigate'|'redraw'} Help4.widget.help.view2.View.WidgetUpdateTypes
     */

    /**
     * @augments Help4.jscore.ControlBase
     * @property {Help4.widget.help.Widget} __widget
     * @property {boolean} __whatsnew
     * @property {boolean} blockUpdate
     * @property {boolean} stealth
     * @property {Help4.control2.container.Container} _contentView
     * @property {Help4.control2.container.Container} _tileView
     * @property {Help4.widget.help.view2.View.Cache} _cache
     * @property {Help4.jscore.MutualExclusion} _mutual
     * @property {boolean} _updating
     * @property {Help4.widget.help.view2.TileContainer} _tileContainer
     * @property {Help4.control2.Control} _mouseOverControl
     * @property {?Help4.widget.help.view2.LaserBeam} _laserBeam
     * @property {Function} _domRefreshExecutor
     * @property {boolean} _initialized
     */
    Help4.widget.help.view2.View = class extends Help4.jscore.ControlBase {
        /**
         * @override
         * @param {Help4.widget.help.view2.View.Params} params
         */
        constructor(params) {
            const domRefreshExecutor = () => this.isDestroyed() || this._updating || this.update();

            const {
                jscore: {ControlBase: {TYPES: T}},
                control2: {container: {Container: ControlContainer}},
                widget: {
                    companionCore: {Core},
                    help: {view2: {TileContainer}},
                    whatsnew: {Widget: WhatsNewWidget}
                }
            } = Help4;

            const {widget} = params;

            const contentViewParams = Core.addStandardViewParameters(widget, {
                css: 'widget-help-view content-view content-view2',
                ariaLive: 'assertive',
                ariaAtomic: 'true',
                ariaRelevant: 'additions'
            }, 'full');

            const tileViewParams = Core.addStandardViewParameters(widget, {
                css: 'widget-help-view tile-view tile-view2'
            }, 'contentDiv');
            tileViewParams.dom ||= document.createDocumentFragment();

            const contentView = new ControlContainer(contentViewParams)
            .addListener(['click', 'mouseover', 'mouseout'], ({type, target: [container, control]}) => _onControlEvent.call(this, type, control));

            const tileView = new ControlContainer(tileViewParams)
            .addListener(['click', 'mouseover', 'mouseout', 'space', 'enter'], ({type, target: [container, control]}) => {
                if (type === 'space' || type === 'enter') type = 'click';
                _onControlEvent.call(this, type, control);
            });

            super(params, {
                params: {
                    widget:      {type: T.instance, readonly: true, mandatory: true, private: true},
                    whatsnew:    {type: T.boolean, readonly: true, private: true},  // will be set below to avoid override by params
                    blockUpdate: {type: T.boolean},
                    stealth:     {type: T.boolean}
                },
                statics: {
                    _contentView:        {init: contentView},
                    _tileView:           {init: tileView},
                    _cache:              {init: {}, destroy: false},
                    _mutual:             {init: new Help4.jscore.MutualExclusion()},
                    _updating:           {init: false, destroy: false},
                    _tileContainer:      {init: null},
                    _mouseOverControl:   {init: null, destroy: false},
                    _laserBeam:          {},
                    _domRefreshExecutor: {init: domRefreshExecutor, destroy: false},
                    _initialized:        {init: false},
                },
                config: {
                    onPropertyChange: ({name, value}) => name === 'stealth' && (this._tileContainer.stealth = value)
                }
            });

            const whatsnew = widget instanceof WhatsNewWidget;
            this.dataFunctions.set({whatsnew}, {allowReadonlyOverride: true});  // allow readonly override

            const {stealth} = this;
            this._tileContainer = new TileContainer({view: this, widget, contentView, tileView, stealth})
            .addListener('closeCallout', ({descriptor}) => _stopCallout.call(this, descriptor))
            .addListener('closeAnnouncement', ({descriptor, checkbox}) => _stopAnnouncement.call(this, descriptor, checkbox))
            .addListener('showCallout', ({descriptor}) => {
                const {view2} = Help4.widget.help;
                const data = view2.convertTileDescriptor(descriptor);
                this._fireEvent({type: 'select', data});
            });
        }

        /** @type {number} */
        static BUBBLE_CLOSE_TIME = 500;

        /** @override */
        destroy() {
            const {Core} = Help4.widget.companionCore;
            const {__widget, _domRefreshExecutor} = this;
            const context = __widget.getContext();
            context && Core.disconnectDomRefresh(_domRefreshExecutor, context);

            super.destroy();
        }

        /**
         * @param {?Help4.widget.help.view2.SerializedStatus} status
         * @param {Help4.widget.help.view2.Tile.Descriptor} [select]
         * @returns {Promise<void>}
         */
        async init(status, select) {
            const {Core} = Help4.widget.companionCore;
            const {__widget, _domRefreshExecutor} = this;

            // system is still booting and might not be ready especially for condition element evaluation;
            // wait a bit to let the boot finish
            await Core.waitControllerPlaybackServiceReady(__widget, 100);
            if (this.isDestroyed()) return;

            // observe dom changes
            const context = __widget.getContext();
            Core.observeDomRefresh(_domRefreshExecutor, context, this);

            // update view
            this._initialized = true;
            await this.update({status, select, isRefresh: false});
        }

        /**
         * @returns {Help4.widget.help.view2.SerializedStatus}
         */
        serialize() {
            const {view2} = Help4.widget.help;
            return view2.serialize(this);
        }

        /**
         * @param {Object} [params = {}]
         * @param {?Help4.widget.help.view2.SerializedStatus} [params.status]
         * @param {Help4.widget.help.view2.Tile.Descriptor} [params.select]
         * @param {boolean} [params.stealth]
         * @param {boolean} [params.isRefresh = true]
         * @returns {Promise<void>}
         */
        async update({status, select, stealth, isRefresh = true} = {}) {
            const {
                help: {view2},
                companionCore: {data: {Help}},
            } = Help4.widget;

            view2.deserialize(this, status);
            _updateTileView.call(this);

            if (this.blockUpdate || !this._initialized) return;

            // check whether we are still showing the same projects with same catalogue key on the same screen
            // if yes, we can skip the complete recalculation and increase performance
            const {
                screenId,
                catalogueKey,
                helpIds,
                isDataUpdate
            } = /** @type {Help4.widget.help.view2.AllowUpdateResult} */ view2.allowUpdateFromCache(this);

            const {
                /** @type {Help4.widget.help.Widget} */ __widget,
                /** @type {Help4.widget.help.view2.View.Cache} */ _cache,
                /** @type {Help4.jscore.MutualExclusion} */ _mutual,
                /** @type {Help4.widget.help.view2.TileContainer} */ _tileContainer,
                /** @type {boolean} */ __whatsnew: whatsnew
            } = this;

            // update cache
            _cache.screenId = screenId;
            _cache.catalogueKey = catalogueKey;
            _cache.helpIds = [...helpIds];
            if (typeof select === 'string') _cache.select = select;
            if (typeof stealth === 'boolean') _cache.stealth = stealth;
            isDataUpdate && (_cache.isDataUpdate = true);

            // start of ASYNC area
            const mutualToken = _mutual.start();
            _mutual.access(mutualToken);  // immediately access; to avoid code execution below

            // block intermediate updates through DomRefreshEngine, etc.
            this._updating = true;

            // create tile list
            const tiles = await Help.getAvailableTiles({whatsnew, screenId});
            if (this.isDestroyed() || !_mutual.access(mutualToken)) return;
            _cache.tiles = tiles;

            // check cache sanity
            if (status && view2.fixPersistentCache(this, tiles, screenId)) {
                this._fireEvent({type: 'fixStatus'});
            }

            // filter UR tiles; in case a UACP/SEN tile is hidden by condition a UR tile with same hotspot will also be hidden
            const urFilteredTiles = Help.filterUrTiles(tiles);

            // reduce to those that pass conditions
            const conditionTiles = await Help.filterTilesByCondition(urFilteredTiles, {whatsnew});
            if (this.isDestroyed() || !_mutual.access(mutualToken)) return;
            _cache.conditionTiles = conditionTiles;

            // update hotspot status
            const tilesWithHotspots = conditionTiles.filter(projectTile => view2.hasHotspot(__widget, projectTile));
            const hotspotStatus = await view2.HotspotScan.scan(__widget, tilesWithHotspots);
            if (this.isDestroyed() || !_mutual.access(mutualToken)) return;

            // end of ASYNC area

            select = _cache.select;
            stealth = _cache.stealth ?? this.stealth;
            delete _cache.select;
            delete _cache.stealth;

            // add to tile container
            _tileContainer.set({projectTiles: conditionTiles, hotspotStatus, stealth});
            if (select) {
                _tileContainer.selected = select;

                const data = view2.convertTileDescriptor(select);
                this._fireEvent({type: 'select', data});
            }

            // set unseen announcements and callouts

            const {configuration: {WM}} = __widget.getContext();
            _tileContainer.announcements = WM > 1 ? [] : _getAnnouncements.call(this);
            _tileContainer.callouts = _getCallouts.call(this);

            // reset status; but not for certain special updates
            if (_cache.isDataUpdate) {
                // XRAY-6395, XRAY-6396
                // several processes might hit the update function
                // one of them could be after a navigate with isDataUpdate true
                // but it could have been kicked out by a later process
                // we store the need to update data once in cache
                // to execute here
                delete _cache.isDataUpdate;
                this.onWidgetUpdate({type: 'navigate', select});
            }

            this.stealth = stealth;

            // unblock intermediate updates
            this._updating = false;
        }

        /** @returns {Help4.widget.help.TileDescriptor[]} */
        getSeenAnimations() {
            const {__widget, _cache} = this;
            const {configuration: {core: {screenId}}} = __widget.getContext();
            return _cache.persist?.animation?.[screenId] || [];
        }

        /** @returns {{tiles: ?Object, content: ?Object}} */
        getTexts() {
            const {_tileView, _contentView} = this;
            return {
                tiles: _tileView?.getTexts(),
                content: _contentView?.getTexts()
            };
        }

        /** @param {{tiles: ?Object, content: ?Object}} texts */
        setTexts({tiles, content}) {
            const {_tileView, _contentView} = this;
            tiles && _tileView?.setTexts(tiles);
            content && _contentView?.setTexts(content);
        }

        /** will close any open lightbox */
        closeLightbox() {
            const {_tileContainer} = this;
            const {selected} = _tileContainer;
            if (!selected) return;

            const {type, linkLightbox} = _tileContainer.get(selected);
            if (type === 'link' && !!linkLightbox) _tileContainer.selected = null;
        }

        /** @param {Help4.widget.help.TileDescriptor} data */
        select(data) {
            const {view2} = Help4.widget.help;
            this._tileContainer.selected = view2.convertTileDescriptor(data);
            this._fireEvent({type: 'select', data});
        }

        /** focus handling */
        focus() {
            const {_tileView} = this;
            _tileView.count() > 0
                ? _tileView.get(0).focus()
                : _tileView.focus();
        }

        /**
         * focus handling - up/down arrow keys
         * @param {string} direction
         */
        focusListItem(direction) {
            const {_tileView} = this;
            let count = _tileView.count();
            if (count > 0) {
                const visibleControls = [];
                _tileView.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();
            }
        }

        /** @param {Help4.widget.help.TileDescriptor} data */
        showQuickTour(data) {
            const {view2} = Help4.widget.help;
            this._tileContainer.quickTour = view2.convertTileDescriptor(data);
            this._fireEvent({type: 'select', data});
        }

        /**
         * @param {Object} event
         * @param {Help4.widget.help.view2.View.WidgetUpdateTypes} event.type
         * @param {Help4.widget.help.view2.Tile.Descriptor} [event.select]
         */
        onWidgetUpdate({type, select}) {
            const {_tileContainer} = this;

            const selectAnnouncementOrCallout = select => {
                const activeWidget = Help4.widget.getActiveInstance();
                if (!activeWidget || activeWidget.getName() !== 'tour') {  // do not interfere w/ tour playback
                    _tileContainer.showAnnouncement();
                    select || _tileContainer.showCallout(null);  // do not use current callout but force a reset
                }
            }

            switch (type) {
                case 'navigate':
                    // navigate means a complete data switch; is called from update()
                    _tileContainer.selected = select || null;
                    _tileContainer.hovered = null;
                    _tileContainer.announcement = null;  // allow a new announcement to be shown
                    _tileContainer.callout = null;  // reset callout data
                    selectAnnouncementOrCallout(select);
                    break;

                case 'widgetDeactivate': { // another widget deactivates
                    const {selected} = _tileContainer;
                    selectAnnouncementOrCallout(selected);
                    break;
                }

                case 'activate':  // widget activates
                case 'controllerOpen':  // help is opened
                    // in case nothing is selected: show next callout
                    const {selected} = _tileContainer;
                    selected || _tileContainer.showCallout();
                    break;

                case 'deactivate':  // widget deactivates
                case 'controllerClose':  // help is closed
                    // clean all content and reset selection
                    // but keep callouts open / open any
                    _tileContainer.hovered = null;

                    _tileContainer.showCallout();
                    if (!_tileContainer.callout) _tileContainer.selected = null;
                    break;

                case 'playbackActive':  // playbackActive is called after init when help is closed
                case 'redraw':  // widget redraw, e.g. API data change, PUB - HEAD switch, ...
                    break;
            }
        }

        /** @returns {Help4.control2.container.Container} */
        getContentView() {
            return this._contentView;
        }

        /** @returns {Help4.widget.help.ProjectTile[]} */
        getTiles() {
            return this._cache.tiles;
        }

        /** @param {Help4.widget.help.view2.Tile.Descriptor} descriptor */
        tileSelect(descriptor) {
            const {view2} = Help4.widget.help;

            _stopHotspotAnimation.call(this, descriptor);

            const {_cache: {tiles}} = this;
            const data = view2.convertTileDescriptor(descriptor);
            const {type} = tiles.find(({id}) => id === data.tileId) || {};
            if (type === 'tour') {
                data.type = 'tour';
                this._fireEvent({type: 'select', data});
                return;
            }

            // select a new tile or deselect an already selected one
            // - selected: TileDescriptor
            // - deselected: null
            // - no change: undefined
            const result = _select.call(this, descriptor);
            if (result !== undefined) {
                _stopCallout.call(this, descriptor);
                const data = result ? view2.convertTileDescriptor(result) : null;
                this._fireEvent({type: 'select', data});
            }
        }

        /**
         * @param {?Help4.widget.help.view2.Tile.Descriptor} [descriptor = null]
         */
        wmHotspotHover(descriptor = null) {
            this._tileContainer.wmHovered = descriptor;
        }
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {'click'|'mouseover'|'mouseout'} eventType
     * @param {Help4.control2.Control} control
     */
    function _onControlEvent(eventType, control) {
        if (!control || control.isDestroyed() || this.isDestroyed()) return;

        const {view2} = Help4.widget.help;
        const descriptor = view2.extractTileDescriptor(control);

        const {Tile: TileControl, hotspot: {Connected: HotspotControl}} = Help4.control2;
        const {BubbleControl} = Help4.widget.help.view2;

        const isTileControl = control instanceof TileControl;
        const isHotspotControl = control instanceof HotspotControl;
        const isBubbleControl = control instanceof BubbleControl;
        const controlType = isTileControl ? 'tile' : isHotspotControl ? 'hotspot' : isBubbleControl ? 'bubble' : null;

        const type = {
            tile: {click: 'tileSelect', mouseover: 'tileOver', mouseout: 'tileOut'},
            hotspot: {click: 'hotspotClick', mouseover: 'hotspotOver', mouseout: 'hotspotOut'},
            bubble: {click: 'bubbleClick', mouseout: 'bubbleOut'}
        }[controlType]?.[eventType];

        if (!type) return;

        switch (eventType) {
            case 'click':
                _onControlEvent2.call(this, type, descriptor, control);
                break;
            case 'mouseover':
                // do not send multiple mouseover events for the same control
                if (this._mouseOverControl !== control) {
                    this._mouseOverControl = control;
                    _onControlEvent2.call(this, type, descriptor, control);
                }
                break;
            case 'mouseout':
                // ignore mouseout events where the mouse is still over
                // wait some time to allow DOM to adapt
                setTimeout(() => {
                    if (control.isMouseOver()) return;
                    this._mouseOverControl = null;
                    _onControlEvent2.call(this, type, descriptor, control);
                }, 50);  // 10ms did not work
                break;
        }
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {string} type
     * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
     */
    function _onControlEvent2(type, descriptor) {
        const {view2} = Help4.widget.help;
        const {_tileContainer: {selected}} = this;

        switch (type) {
            case 'hotspotClick':
                _stopCallout.call(this, descriptor);
                // fall-through is intentional
            case 'tileSelect':
                this.tileSelect(descriptor);
                break;
            case 'bubbleClick':
                _stopCallout.call(this, descriptor);
                break;

            case 'tileOver':
                view2.LaserBeam.show(this, descriptor);
                break;

            case 'tileOut':
                // ATTENTION:
                // tileOut comes with a delay to ensure that mouse is truly out
                // therefore tileOver and tileOut events come out-of-order
                view2.LaserBeam.hide(this, descriptor);
                break;

            case 'hotspotOver': {
                // select a new tile or deselect an already selected one
                // - selected: TileDescriptor
                // - deselected: null
                // - no change: undefined
                _stopHotspotAnimation.call(this, descriptor);
                const result = _hotspotHover.call(this, descriptor);
                if (result !== undefined && !selected) {
                    // in case nothing is selected: hotspot hover counts as selection
                    const data = result ? view2.convertTileDescriptor(result) : null;
                    this._fireEvent({type: 'select', data});
                }
                break;
            }

            case 'hotspotOut':
            case 'bubbleOut':
                // ATTENTION:
                // hotspotOut comes with a delay to ensure that mouse is truly out
                // therefore hotspotOver and hotspotOut events come out-of-order
                _leave.call(this, descriptor)
                .then(success => {
                    if (!success) return;

                    const data = view2.convertTileDescriptor(descriptor);
                    this._fireEvent({type: 'select', data: null, oldData: data})
                });
                break;
        }
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor|null} descriptor
     * @returns {Help4.widget.help.view2.Tile.Descriptor|null|undefined}
     */
    function _select(descriptor) {
        // return values
        // - selected: TileDescriptor
        // - deselected: null
        // - no change: undefined

        const {_tileContainer} = this;
        const {/** @type {Help4.widget.help.view2.Tile.Descriptor|null} */ selected} = _tileContainer;
        const isAlreadySelected = selected && selected === descriptor || false;

        // deselect or click to an already selected item
        if (!descriptor || isAlreadySelected) {
            if (selected) {
                // deselect
                _tileContainer.selected = null;
                _tileContainer.hovered = null;  // deselection also removes hover state
                return null;
            }
            return undefined;  // no change
        }

        // new selection
        _tileContainer.selected = descriptor;
        return descriptor;
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor|null} descriptor
     * @returns {Help4.widget.help.view2.Tile.Descriptor|null|undefined}
     */
    function _hotspotHover(descriptor) {
        // return values
        // - hovered: TileDescriptor
        // - un-hovered: null
        // - no change: undefined

        const {_tileContainer} = this;
        const {
            /** @type {Help4.widget.help.view2.Tile.Descriptor|null} */ selected,
            /** @type {Help4.widget.help.view2.Tile.Descriptor|null} */ hovered
        } = _tileContainer;

        if (selected) {
            // something is selected; do not interfere with it

            if (selected === descriptor) {
                // set hover already selected hotspot
                _tileContainer.hovered = descriptor;
                return descriptor;
            }

            return undefined;  // no change
        }

        const isAlreadyHovered = hovered && hovered === descriptor || false;
        if (isAlreadyHovered) return undefined;  // no change

        _tileContainer.hovered = descriptor;
        return descriptor;
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor|null} descriptor
     * @returns {Promise<boolean>}
     */
    async function _leave(descriptor) {
        const {_tileContainer} = this;
        const {
            /** @type {Help4.widget.help.view2.Tile.Descriptor|null} */ selected,
            /** @type {Help4.widget.help.view2.Tile.Descriptor|null} */ hovered
        } = _tileContainer;

        const isHovered = hovered && hovered === descriptor;
        if (!isHovered) return false;

        if (selected) {
            // something is selected; do not interfere with it
            // just remove the hover mark
            _tileContainer.hovered = null;
            return false;
        }

        // in case a hotspot out results in a closed bubble:
        // hotspot out counts as deselect

        // do not use "_tileContainer.hovered = null;" as we need delayed close functionality!
        return await _tileContainer.leave();
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
     */
    async function _stopHotspotAnimation(descriptor) {
        if (this._tileContainer.stopHotspotAnimation(descriptor)) {
            const {view2} = Help4.widget.help;
            if (await view2.setPersistenceCacheValue(this, 'animation', descriptor)) {
                this._fireEvent({type: 'stopAnimation'});
            }
        }
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
     */
    async function _stopCallout(descriptor) {
        const {view2} = Help4.widget.help;

        const {_tileContainer} = this;
        const isCallout = Help4.includes(_tileContainer.callouts, descriptor);

        if (isCallout && await view2.setPersistenceCacheValue(this, 'callout', descriptor)) {
            // callout no longer open
            _tileContainer.callout = null;

            // update callout list; now one more callout has been seen
            _tileContainer.callouts = _getCallouts.call(this);

            // update storage
            this._fireEvent({type: 'stopCallout'});
        }
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
     * @param {boolean|null} checkbox
     */
    async function _stopAnnouncement(descriptor, checkbox) {
        const {view2} = Help4.widget.help;

        const {_tileContainer, _cache: {conditionTiles}} = this;
        if (_tileContainer.announcement !== descriptor) return;

        const {tileId} = view2.convertTileDescriptor(descriptor);
        const {splashOption} = checkbox === true
            ? {splashOption: 'once'}  // checkbox true: do not show again
            : /** @type {Help4.widget.help.ProjectTile} */ conditionTiles.find(({id}) => id === tileId);

        const persistenceId = `announcement_${splashOption}`;

        if (await view2.setPersistenceCacheValue(this, persistenceId, descriptor)) {
            // announcement no longer open; set empty string to avoid showing any further announcements
            _tileContainer.announcement = false;

            // update callout list; now one more callout has been seen
            _tileContainer.announcements = _getAnnouncements.call(this);

            // update storage
            this._fireEvent({type: 'stopAnnouncement'});
        }
    }

    /**
     * @memberof Help4.widget.help.view2.View#
     * @private
     */
    function _updateTileView() {
        const {__widget, _tileView} = this;

        if (__widget.isActive()) {
            const {panel} = __widget.getContext();
            if (panel) {
                const contentInstance = /** @type {?Help4.control2.bubble.content.Panel} */ panel.getContentInstance();
                _tileView.setDom(contentInstance?.useContentDiv());
                return;
            }
        }

        const ownerDom = _tileView.getOwnerDom();
        if (ownerDom.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
            _tileView.setDom(document.createDocumentFragment());
        }
    }

    /**
     * get unseen callouts
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @returns {Help4.widget.help.view2.Tile.Descriptor[]}
     */
    function _getCallouts() {
        const {view2} = Help4.widget.help;
        const {_cache, __widget} = this;
        const {persist: {callout} = {}, conditionTiles} = _cache;
        const {configuration: {core: {screenId}}} = __widget.getContext();

        return conditionTiles
        .map(tile => {
            if (!!tile.callout) {
                // filter already seen callouts
                const descriptor = view2.extractTileDescriptor(tile);
                return view2.containsTileDescriptor(callout?.[screenId] || [], descriptor)
                    ? null
                    : descriptor;
            }
            return null;
        })
        .filter(descriptor => !!descriptor);
    }

    /**
     * get unseen announcements
     * @memberof Help4.widget.help.view2.View#
     * @private
     * @returns {Help4.widget.help.view2.Tile.Descriptor[]}
     */
    function _getAnnouncements() {
        const {view2} = Help4.widget.help;
        const {_cache, __widget} = this;
        const {persist: {announcement_always, announcement_once} = {}, conditionTiles} = _cache;
        const {configuration: {core: {screenId}}} = __widget.getContext();

        const tile = conditionTiles.find(tile => {
            const {type, linkLightbox, linkTo, splash} = tile;
            return type === 'link' && !!linkLightbox && !!linkTo && !!splash;
        });

        if (tile) {
            // always check both lists; as an "always" announcement is stored within "once"
            // in case the user checks the "do not show again" checkbox
            const descriptor = view2.extractTileDescriptor(tile);
            if (!view2.containsTileDescriptor(announcement_always?.[screenId] || [], descriptor) &&
                !view2.containsTileDescriptor(announcement_once?.[screenId] || [], descriptor))
            {
                return [descriptor];
            }
        }

        return [];
    }
})();