Source: widget/tour/BubbleController.js

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

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

        /**
         * @param {string} [reason = 'next']
         * @returns {Promise<void>}
         */
        async create(reason = 'next') {
            const {/** @type {Help4.widget.tour.View} */ _view} = this;

            /**
             * {@link Help4.controller.Tour}; see showBubble
             * {@link Help4.controller.helper.Bubbles.prototype.openTour}
             * {@link Help4.service.container.BubbleService.TourBubble.open}
             */

            /** @type {Help4.widget.help.ProjectTile} */ const tile = _view.getCurrentTile();
            /** @type {Help4.widget.help.Project} */ const {language} = _view.getProject();
            /** @type {number} */ const steps = _view.getSteps();
            /** @type {number} */ const step = _view.getStep();
            const {
                /** @type {Help4.controller.Controller} */ controller,
                /** @type {Help4.typedef.SystemConfiguration} */ configuration
            } = _view.getContext();

            const {activeMLTranslation, translationAvailable} = configuration.translation;
            // /** @type {string} */ const projectLanguage = Help4.getShell().getLanguage(_view.getProject().language).uacp;
            // const systemLanguage = configuration.core.language.uacp;
            // const showTranslateButton = translationAvailable && projectLanguage !== systemLanguage;
            const showArrow = tile.showArrow || false;

            /**
             * {@link Help4.service.container.BubbleService.TourBubble.open}
             */
            // TODO mlt remove this. handle in engine
            const title = (activeMLTranslation && false) && tile._translation.title || tile.title;

            const zoomService = controller.getService('zoom');
            const mlt = controller.getEngine('MLT');
            const controlParams = {
                _metadata: {tileId: tile.id, type: 'bubble'},
                controlType: 'Help4.widget.tour.BubbleControl',
                autoFocus: false,  // XRAY-1872
                caption: Help4.Placeholder.resolve(title),
                showCaption: tile.showTitleBar,
                content: _prepareContent.call(this, tile),
                modal: false,
                showArrow,
                size: tile.bubbleSize,
                appearance: tile.bubbleType,
                // orientation: tile.bubbleOrientation,  // XXX: needed???
                animationType: tile.bubbleAnimationType,
                showPrevButton: _view.isPrevStepAvailable(),
                showNextButton: _hasNextButton.call(this),
                showFinishButton: step + 1 === steps,
                contentLanguage: language,
                enableTranslation: mlt.isTranslationActive(),
                activeTranslation: mlt.isTranslationActive(),
                zoomService
            };

            /** @type {Help4.widget.tour.BubbleControl} */ const bubble = _view.add(controlParams);
            mlt.registerBubble(bubble);

            bubble
            .addListener('close', () => _view.stop())
            .addListener('next', () => _view.nextStep())
            .addListener('prev', () => _view.prevStep())
            .addListener('translate', () => mlt.onBubbleClick({bubble, active: !bubble.activeTranslation}))
            .addListener('destroy', () => mlt.unregisterBubble(bubble))
            .addListener('dragdrop', () => {
                bubble.setMetadata('dragdrop', true);
                bubble.showArrow = false;
            });

            // XRAY-3807, XRAY-1180: align after scroll and after image/video load
            const {Element, jscore: {MediaWatcher}} = Help4;
            const dom = bubble.getDom();
            await Element.execAfterTransition(dom);
            if (!bubble.isDestroyed()) {
                await MediaWatcher.observe(dom);
                bubble.isDestroyed() || _align.call(this);
            }
        }

        /** @returns {Help4.widget.tour.BubbleController} */
        update() {
            _align.call(this);

            /** @type {?Help4.widget.tour.BubbleControl} */ const bubble = this.get();
            if (bubble) bubble.showNextButton = _hasNextButton.call(this);

            return this;
        }

        /**
         * called after navigate to an unknown screen
         * @returns {boolean}
         */
        invalidate() {
            /** @type {?Help4.widget.tour.BubbleControl} */ const bubble = this.get();
            if (bubble) {
                const {/** @type {Help4.widget.tour.View} */ _view} = this;
                bubble.showArrow = false;
                bubble.showPrevButton = false;
                return true;
            }
            return false;
        }
    }

    /**
     * @memberof Help4.widget.tour.BubbleController#
     * @private
     * @returns {boolean}
     */
    function _hasNextButton() {
        const {/** @type {Help4.widget.tour.View} */ _view} = this;

        const {autoProgress} = _view.getCurrentTile();
        const {visible: curVisible} = _view.getCurrentStatus();

        /** @type {number} */ let step = _view.getStep();
        while (true) {
            // check whether next step exists and is on same screen
            if (!_view.isNextStepAvailable(step++)) return false;

            // check whether next step is visible
            const {autoSkipStep} = _view.getTile(step);
            const {visible} = _view.getStatus(step);
            if (autoSkipStep && !visible) continue;  // XRAY-1808: next item is not visible but can be skipped

            // XRAY-133: only show next if allowed/configured
            // XRAY-1871: always show next button if item is not visible
            return !curVisible || autoProgress.includes('next');
        }
    }

    /**
     * @memberof Help4.widget.tour.BubbleController#
     * @private
     * @param {Help4.widget.help.ProjectTile} tile
     * @returns {string}
     */
    function _prepareContent(tile) {
        const {/** @type {Help4.widget.tour.View} */ _view} = this;

        const {
            /** @type {Help4.controller.Controller} */ controller,
            /** @type {Help4.typedef.SystemConfiguration} */ configuration
        } = _view.getContext();
        const {activeMLTranslation} = configuration.translation;

        // XXX: implement MLT
        /** {@link Help4.service.container.BubbleService.prototype.prepareContent} */
        const content = (activeMLTranslation && false) && tile._translation?.content || tile.content;

        const {_ctx: ctx, _mac: mac} = tile;
        const text = Help4.Placeholder.resolve(content);
        return Help4.control.input.HtmlEditor.transformForPlayback({text, ctx, mac, controller});
    }

    /**
     * @memberof Help4.widget.tour.BubbleController#
     * @private
     */
    function _align() {
        const {/** @type {Help4.widget.tour.View} */ _view} = this;

        /** @type {?Help4.widget.tour.BubbleControl} */ const bubble = this.get();
        if (!bubble || bubble.getMetadata('dragdrop')) return;  // nothing to align or XRAY-2353

        const {
            bubbleOffset,
            bubbleOrientation: orientation,
            showArrow
        } = /** @type {Help4.widget.help.ProjectTile} */ _view.getCurrentTile();
        /** @type {?Help4.control2.hotspot.Connected} */ const hotspot = _view.getHotspotControl();
        /** @type {?Help4.control2.ConnectionPoints} */ const hotspotConnectionPoints = hotspot?.getConnectionPoints({useMidPoint: true});

        if (hotspotConnectionPoints) {
            const ori = Help4.control2.bubble.ORI_MAP[orientation];  // XRAY-1446: bubble offset
            if (ori && bubbleOffset) {
                /** @type {Help4.control2.PositionXY} */
                const point = hotspotConnectionPoints.c || hotspotConnectionPoints[ori.charAt(0)];
                if (point) {
                    const {left: x, top: y} = bubbleOffset;
                    point.x += x;
                    point.y += y;
                }
            }

            // align to hotspot as good as possible
            /** @type {?Help4.control2.bubble.Alignment} */
            const alignment = bubble.align(hotspotConnectionPoints, {orientation});  // XRAY-1425, XRAY-1054
            if (Help4.includes(['l', 'r', 't', 'b', 'm'], alignment.orientation)) {
                bubble.showArrow = showArrow;
                return;
            }
        }

        // align centered
        bubble.align();
        bubble.showArrow = false;
    }
})();