Source: widget/help/view2/TileBubble.js

(function() {
    /** bubble helper for {@link Help4.widget.help.view2.Tile} */
    Help4.widget.help.view2.TileBubble = class {
        /** @param {Help4.widget.help.view2.Tile} tile */
        static handleBubble(tile) {
            const {projectTile: {type, _catalogueType}, _selected, _hovered, __contentView, bubbleControlId, stealth} = tile;
            if (type !== 'help') return;

            const {CATALOGUE_TYPE: GlobalHelp_CATALOGUE_TYPE} = Help4.widget.help.catalogues.GlobalHelp;
            const isAlreadyOpen = !!(bubbleControlId && __contentView.get(bubbleControlId));
            const isGlobalHelp = _catalogueType === GlobalHelp_CATALOGUE_TYPE;

            (_selected || _hovered) && !stealth
                ? isAlreadyOpen || (isGlobalHelp ? _openGlobalHelp(tile) : _openBubble(tile))
                : isAlreadyOpen && this.closeBubble(tile);
        }

        /** @param {Help4.widget.help.view2.Tile} tile */
        static alignBubble(tile) {
            const {__contentView, bubbleControlId, hotspotControlId, projectTile} = tile;

            const bubble = bubbleControlId && __contentView.get(bubbleControlId);
            if (!bubble || bubble.getMetadata('dragdrop')) return;  // do not align a bubble that has been moved by the user

            let alignment;
            const hotspot = hotspotControlId && __contentView.get(hotspotControlId);
            const points = /** @type {?Help4.control2.ConnectionPoints} */ hotspot?.visible ? hotspot.getConnectionPoints() : null;
            if (points) {  // align to hotspot
                const {ORI_MAP} = Help4.control.bubble;
                const {bubbleOrientation: orientation, bubbleOffset} = projectTile;
                let ori = /** @type {string} */ ORI_MAP[orientation];  // XRAY-1446: bubble offset

                if (ori && bubbleOffset) {
                    const modify = points[ori.charAt(0)];
                    if (modify) {
                        modify.x += bubbleOffset.left;
                        modify.y += bubbleOffset.top;
                    }
                }

                alignment = bubble.align(points, {ignoreArea: true, orientation});
            }

            if (alignment) {
                bubble.addCss('hotspot');  // specific class in order to beautify hotspot alignment
            } else {
                // align centered
                bubble.removeCss('hotspot');  // specific class in order to beautify hotspot alignment
                bubble.align();
            }
        }

        /** @param {Help4.widget.help.view2.Tile} tile */
        static closeBubble(tile) {
            const {__contentView, bubbleControlId} = tile;
            bubbleControlId && __contentView.remove(bubbleControlId);
            tile.bubbleControlId = null;
        }
    }

    /**
     * @private
     * @param {Help4.widget.help.view2.Tile} tile
     */
    function _openBubble(tile) {
        const {
            control: {input: {HtmlEditor}},
            Placeholder
        } = Help4;

        const {__contentView, __widget, projectTile, descriptor} = tile;

        const {controller} = __widget.getContext();
        const {
            _ctx: ctx,
            _mac: mac,
            _isUR: isUR,
            showTitleBar: showCaption,
            bubbleSize: size,
            bubbleAnimationType: animationType,
            language
        } = projectTile;
        let {
            title: caption,
            content,
            showArrow
        } = projectTile;
        caption = Placeholder.resolve(caption);
        content = HtmlEditor.transformForPlayback({
            text: Placeholder.resolve(content),
            ctx,
            mac,
            controller
        });
        showArrow ??= true;

        const zoomService = controller.getService('zoom');
        const mlt = controller.getEngine('MLT');
        const bubbleParams = {
            _metadata: {descriptor, type: 'bubble', isUR},
            controlType: 'Help4.widget.help.view2.BubbleControl',
            caption,
            content,
            showArrow,
            showCaption,
            size,
            animationType,
            contentLanguage: language,
            enableTranslation: !isUR && mlt.isTranslationActive(),
            activeTranslation: !isUR && mlt.isTranslationActive(),
            zoomService,
            autoFocus: false
        };

        const bubble = /** @type {Help4.widget.help.view2.BubbleControl} */ __contentView.add(bubbleParams);
        tile.bubbleControlId = bubble.id;
        mlt.registerBubble(bubble);

        const {TileBubble} = Help4.widget.help.view2;

        bubble
        .addListener('translate', () => mlt.onBubbleClick({bubble, active: !bubble.activeTranslation}))
        .addListener('destroy', () => mlt.unregisterBubble(bubble))
        .addListener('close', () => {
            TileBubble.closeBubble(tile);

            // fire event to signal a user click to the close button of the bubble
            // this is different from other close reasons that are handled in closeBubble
            tile._fireEvent({type: 'userCloseBubble', descriptor});
        })
        .addListener('dragdrop', () => {
            bubble.setMetadata('dragdrop', true);
            bubble.showArrow = false;
        });

        Help4.WM.enableVideoTracking(bubble.getDom());

        // make sure bubble is properly aligned
        TileBubble.alignBubble(tile);

        // align again once images and videos are loaded
        _alignAfterImageLoad(tile, bubble);
    }

    /**
     * @private
     * @param {Help4.widget.help.view2.Tile} tile
     */
    function _openGlobalHelp(tile) {
        const {
            control: {input: {HtmlEditor}},
            Placeholder
        } = Help4;

        const {__contentView, __widget, projectTile, descriptor} = tile;

        const {controller} = __widget.getContext();
        const {
            _ctx: ctx,
            _mac: mac,
            language
        } = projectTile;
        let {
            title: caption,
            content
        } = projectTile;
        caption = Placeholder.resolve(caption);
        content = HtmlEditor.transformForPlayback({
            text: Placeholder.resolve(content),
            ctx,
            mac,
            controller
        });

        const zoomService = controller.getService('zoom');
        const mlt = controller.getEngine('MLT');
        const bubbleParams = {
            _metadata: {descriptor, type: 'bubble'},
            controlType: 'Help4.widget.help.view2.GlobalHelpDialogControl',
            caption,
            content,
            contentLanguage: language,
            enableTranslation: mlt.isTranslationActive(),
            activeTranslation: mlt.isTranslationActive(),
            zoomService
        };

        const bubble = /** @type {Help4.widget.help.view2.GlobalHelpDialogControl} */ __contentView.add(bubbleParams);
        tile.bubbleControlId = bubble.id;
        mlt.registerBubble(bubble);

        const {TileBubble} = Help4.widget.help.view2;

        bubble
        .addListener('close', () => TileBubble.closeBubble(tile))
        .addListener('translate', () => mlt.onBubbleClick({bubble, active: !bubble.activeTranslation}))
        .addListener('destroy', () => mlt.unregisterBubble(bubble))
        .addListener('dragdrop', () => bubble.setMetadata('dragdrop', true));

        // make sure bubble is properly aligned
        TileBubble.alignBubble(tile);
    }

    /**
     * @private
     * @param {Help4.widget.help.view2.Tile} tile
     * @param {Help4.control2.bubble.Bubble} bubble
     */
    function _alignAfterImageLoad(tile, bubble) {
        const {MediaWatcher} = Help4.jscore;
        const dom = bubble.getDom();

        MediaWatcher.observe(dom)
        .then(result => {
            if (result.length && !bubble.isDestroyed()) {
                bubble.resetAlignmentCache();

                const {TileBubble} = Help4.widget.help.view2;
                TileBubble.alignBubble(tile);
            }
        });
    }
})();