Source: widget/tour/HotspotController.js

(function() {
    /**
     * @typedef {Object} Help4.widget.tour.HotspotController.ControlParams
     * @property {string} animationType
     * @property {Help4.control2.SizeWidthHeight} delta
     * @property {string} hotspotType
     * @property {string} icon
     * @property {?number} number
     * @property {Help4.control2.PositionXY} point
     * @property {string} pos
     * @property {Help4.control2.AreaXYWH} rect
     * @property {string} size
     * @property {boolean} spotlight
     * @property {number} spotlightBlur
     * @property {number} spotlightOffset
     * @property {number} spotlightOpacity
     * @property {?string} title
     * @property {boolean} active
     * @property {boolean} visible
     * @property {Object} _metadata
     * @property {string} _metadata.tileId
     * @property {string} _metadata.hotspotId
     * @property {string} _metadata.controlStyle
     * @property {string} [controlType]
     * @property {string} [ariaLabel]
     */

    /**
     * Hotspot controller for tour playback view
     * @augments Help4.jscore.Base
     * @property {Help4.widget.tour.View} _view
     */
    Help4.widget.tour.HotspotController = class extends Help4.jscore.Base {
        /**
         * @override
         * @param {Help4.widget.tour.View} view
         */
        constructor(view) {
            super({
                statics: {
                    _view: {init: view, destroy: false}
                }
            });
        }

        /**
         * get hotspot control
         * @param {?string} [tileId]
         * @returns {?Help4.control2.hotspot.Connected}
         */
        get(tileId) {
            const {Connected} = Help4.control2.hotspot;
            const {/** @type {Help4.widget.tour.View} */ _view} = this;
            for (/** @type {Help4.control2.Control} */ const control of _view) {
                if (control instanceof Connected) {
                    if (!tileId || control.getMetadata('tileId') === tileId) {
                        return control;
                    }
                }
            }
            return null;
        }

        /**
         * remove hotspot control
         * @param {?string} [tileId = null]
         * @returns {?Help4.control2.hotspot.Connected}
         */
        remove(tileId = null) {
            const {Connected} = Help4.control2.hotspot;
            const {/** @type {Help4.widget.tour.View} */ _view} = this;
            for (const [index, /** @type {Help4.control2.Control} */ control] of _view.entries()) {
                if (control instanceof Connected) {
                    if (!tileId || control.getMetadata('tileId') === tileId) {
                        _view.stopElementObservation();
                        return _view.remove(index);
                    }
                }
            }
            return null;
        }

        /**
         * create hotspot control
         * @param {boolean} [isRefresh = false]
         * @returns {Help4.widget.tour.HotspotController}
         */
        create(isRefresh = false) {
            const {/** @type {Help4.widget.tour.View} */ _view} = this;

            /** @type {Help4.widget.help.ProjectTile} */
            const tile = _view.getCurrentTile();
            const {id, hotspotAnchor, hotspotCentered, autoProgress} = tile;

            if (!hotspotAnchor || !_view.isTileOnScreen(tile)) {
                // no hotspot for this step on current screen
                this.remove();
                return this;
            }

            /** @type {string} */
            const existingHotspotId = this.get()?.getMetadata('tileId');

            if (!isRefresh || existingHotspotId !== id) {
                // new hotspot or changed hotspot
                this.remove();  // remove other/old hotspot

                /** @type {?Help4.data.TileData} */ const tileData = _createControl.call(this, tile);

                if (!hotspotCentered && tileData) {
                    if (!isRefresh) _scrollIntoView.call(this, tileData.hotspot);

                    const map = ['click', 'mouseover', 'tab', 'enter', 'keyup', 'blur'];
                    if (map.find(eventName => autoProgress.includes(eventName))) {
                        _view.startElementObservation(tileData);
                    }
                }
            } else if (isRefresh && !hotspotCentered) {
                // same hotspot; update position
                _updateControl.call(this, tile);
            }

            return this;
        }

        /** @returns {Help4.widget.tour.HotspotController} */
        update() {
            this.create(true);
            return this;
        }

        /**
         * sets focus to hotspot element
         * @param {Help4.widget.help.ProjectTile} tile
         * @returns {Promise<boolean>}
         */
        async focusElement(tile) {
            const {hotspotAnchor} = tile;
            if (!hotspotAnchor) return Help4.Promise.resolve(false);  // unassigned/centered tour step. hotspot element not available

            const tileData = Help4.widget.companionCore.Core.tileToTileData(tile);
            const {service: {playbackService}} = this._view.getContext();

            return playbackService.focusElement(tileData.hotspot);
        }
    }

    /**
     * @memberof Help4.widget.tour.HotspotController#
     * @private
     * @param {Help4.widget.help.ProjectTile} tile
     * @returns {?Help4.data.TileData}
     */
    function _createControl(tile) {
        const {/** @type {Help4.widget.tour.View} */ _view} = this;
        const {tileData, controlParams} = _getControlParams.call(this, tile);

        if (controlParams) {
            /** {@link Help4.service.container.HotspotService}; see _add */
            controlParams.controlType = 'hotspot.Connected';
            controlParams.ariaLabel = controlParams.title;
            controlParams.title = null;  // XRAY-5028 - hotspot tooltip - hide in playback
            controlParams._metadata.type = 'hotspot';

            // create hotspot control
            _view.add(controlParams);

            // deliver information
            return tileData;
        }

        // unsuccessful
        return null;
    }

    /**
     * @memberof Help4.widget.tour.HotspotController#
     * @private
     * @param {Help4.widget.help.ProjectTile} tile
     */
    function _updateControl(tile) {
        /** @type {?Help4.control2.hotspot.Connected} */ const hotspotControl = this.get(tile.id);
        if (hotspotControl) {
            const {controlParams} = _getControlParams.call(this, tile);
            if (controlParams) {
                // recorded element is available
                const {
                    /** @type {?Help4.control2.PositionXY} */ point,
                    /** @type {?Help4.control2.AreaXYWH} */ rect,
                    /** @type {boolean} */ visible
                } = /** @type {Help4.widget.tour.View.HotspotStatus} */ controlParams;

                // apply to control
                hotspotControl.visible = visible;
                hotspotControl.point = point;
                hotspotControl.rect = rect;
            } else {
                // recorded element is gone
                this.remove();
            }
        }
    }

    /**
     * {@link Help4.engine.hotspotManager.HotspotManagerEngine.HotspotController}; see _createControl
     * @memberof Help4.widget.tour.HotspotController#
     * @private
     * @param {Help4.widget.help.ProjectTile} tile
     * @returns {{tileData: Help4.data.TileData, controlParams: ?Help4.widget.tour.HotspotController.ControlParams}}
     */
    function _getControlParams(tile) {
        const {/** @type {Help4.widget.tour.View} */ _view} = this;
        const {/** @type {Help4.typedef.SystemConfiguration} */ configuration} = _view.getContext();
        /** @type {Help4.data.TileData} */ const tileData = Help4.widget.companionCore.Core.tileToTileData(tile);
        /** @type {Help4.data.HotspotData} */ const hotspotData = tileData.hotspot;

        // use newest available status
        hotspotData.status = _view.getCurrentStatus();
        /** @type {?Help4.widget.tour.HotspotController.ControlParams} */
        const controlParams = hotspotData.toControlParams(configuration);

        return {tileData, controlParams};
    }

    /**
     * {@link Help4.engine.hotspotManager.HotspotManagerEngine.prototype.scrollIntoView}
     * @memberof Help4.widget.tour.HotspotController#
     * @private
     * @param {Help4.data.HotspotData} hotspotData
     */
    function _scrollIntoView(hotspotData) {
        const {/** @type {Help4.widget.tour.View} */ _view} = this;
        const {
            /** @type {Help4.widget.tour.View.HotspotStatus} */ status,
            /** @type {string} */ hotspotAnchor
        } = hotspotData;
        const {visible, scrolled, topmost} = status;

        if (visible && (scrolled || !topmost)) {
            const {
                service: {/** @type {Help4.service.recording.PlaybackService} */ playbackService},
                /** @type {Help4.controller.Controller} */ controller
            } = _view.getContext();
            playbackService?.scrollIntoView(hotspotData);

            // UR hotspot scrolling is handled by the UR engine through postMessage communication
            // executing both actions in case it was a mock hotspot
            const {Help} = Help4.widget.companionCore.data;
            const value = Help.decodeUrHotspot(hotspotAnchor)?.ur;
            if (value) {
                /** @type {Help4.engine.ur.UrHarmonizationEngine} */
                const urEngine = controller.getEngine('urHarmonization');
                urEngine?.scrollIntoView(value);
            }
        }
    }
})();