Source: controller/CMP4.js

(function() {
    /**
     * @namespace controller
     * @memberof Help4
     */

    /**
     * @typedef {Help4.jscore.Base.Params} Help4.controller.CMP4.Params
     * @property {Help4.controller.Controller} controller
     */

    /**
     * CMP4 controller
     * @augments Help4.jscore.Base
     * @property {Help4.controller.Controller} __controller
     * @property {boolean} docked
     * @property {?Help4.control2.PositionLeftTop} position
     * @property {boolean} started
     * @property {boolean} active
     * @property {?Help4.control2.bubble.Panel} panel
     * @property {string} mltPreferredLanguage
     * @property {boolean} mltTranslationActive
     * @property {string} mltTargetLanguage
     * @property {boolean} _helpAvailable
     * @property {?Help4.observer.EventBusObserver} _statusObserver
     * @property {?Help4.observer.EventBusObserver} _autoStartTourObserver
     * @property {?string} _widget
     * @property {?Object} _cmp3
     */
    Help4.controller.CMP4 = class extends Help4.jscore.ControlBase {
        /**
         * @override
         * @param {Help4.controller.CMP4.Params} params
         */
        constructor(params) {
            const T = Help4.jscore.ControlBase.TYPES;
            super(params, {
                params: {
                    controller: {type: T.instance, private: true, mandatory: true, readonly: true},
                    docked:     {type: T.boolean, init: false},
                    position:   {type: T.leftTop_null, init: null}
                },
                statics: {
                    started:                {init: false, destroy: false},
                    active:                 {init: false, destroy: false},
                    panel:                  {init: null},
                    _helpAvailable:         {init: false, destroy: false},
                    _statusObserver:        {init: null},
                    _autoStartTourObserver: {init: null},
                    _widget:                {init: null, destroy: false},
                    _cmp3:                  {init: null, destroy: false}
                }
            });
        }

        /**
         * @param {Object} [params = {}]
         * @param {boolean} [params.full = true] - whether full storage is enabled
         * @returns {Object}
         */
        serialize({full = true} = {}) {
            const state = Help4.widget.companionCore.State.get();
            const status = {};

            Object.entries(state).forEach(([key, /** @type {Help4.widget.Widget.SerializedStatus} */ widgetStatus]) => {
                status[key] = full ? widgetStatus?.full : widgetStatus?.interaction;
            });

            /** see {@link Help4.controller.Persistence._serializeCMP4} */
            const {__controller} = this;
            const mltEngine = __controller.getEngine('MLT');
            const mltPreferredLanguage = mltEngine?.getPreferredLanguage();
            const mltTranslationActive = mltEngine?.isTranslationActive();
            const mltTargetLanguage = mltEngine?.getTargetLanguage();
            const {docked, position} = this;
            return {mltPreferredLanguage, mltTranslationActive, mltTargetLanguage, docked, position, status};
        }

        /**
         * @param {Object} obj
         * @param {Function} getSerializedData
         * @param {boolean} full
         * @returns {?Object}
         */
        deserialize(obj, getSerializedData, full) {
            if (obj) {
                /** see {@link Help4.controller.Persistence._deserializeCMP4} */
                const data = getSerializedData(obj, 'widget') || {};
                const {mltPreferredLanguage, mltTranslationActive, mltTargetLanguage, docked, position, status} = data;
                this.docked = docked;
                this.position = position;
                this.mltPreferredLanguage = mltPreferredLanguage;
                this.mltTranslationActive = mltTranslationActive;
                this.mltTargetLanguage = mltTargetLanguage;

                if (full) {
                    const active = _getLastActiveWidget(status);
                    if (active) this._widget = active.name;
                }

                const state = {};
                Object.entries(status).forEach(([key, value]) => {
                    state[key] = full ? {full: value} : {interaction: value};
                });

                Help4.widget.companionCore.State.restore(state);
                return data;
            }

            return null;
        }

        /**
         * starts CMP4 infrastructure
         * @returns {Promise<void>}
         */
        async start() {
            if (this.started) return;
            this.started = true;

            // create the panel
            _createPanel.call(this);

            // create container for infobar
            const {__controller} = this;
            __controller.getEngineManager().createInfobar4Service();
            __controller.getEngineManager().createLightbox4Service();

            const {core: {readCatalogue}} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
            // show help button irrespective of help availability and CMP4 activation
            if (readCatalogue) await Help4.widget.Infrastructure.integrate();
        }

        /**
         * activates CMP4 mode
         * @param {Object} [info = {}]
         * @param {Object} [info.cmp3 = {}] - resume info fom CMP3
         * @param {?Object} [info.editor = null] - switch from CMP3 editing to CMP4 playback
         * @param {Function} [info.callback = () => {}]
         * @returns {Promise<void>}
         */
        async activate({cmp3, editor, callback = () => {}} = {}) {
            if (this.active) return;

            cmp3 ||= this._cmp3 || {};
            editor ||= null;
            this._cmp3 = null;

            const {EventBus: {TYPES}} = Help4;
            const {__controller} = this;
            const eventBus = __controller.getService('eventBus');
            const tabs = {
                help: 'help',
                learning: 'learning',
                tourlist: 'tourlist',
                wn_help: 'whatsnew'
            };

            const {core: {readCatalogue}} = __controller.getConfiguration();

            const {STATUS} = Help4.StartStatus;
            const startStatus = __controller.getService('startStatus');
            const hasToggle = startStatus.has(STATUS.toggle);

            if (editor) {
                // switch from editor to playback
                this.active = true;

                _monitorWidgetUpdates.call(this);
                _monitorAutoStartTour.call(this);

                __controller.onMinimize(false);  // XRAY-850, XRAY-4957

                const tabId = __controller.getContext('carouselTab');
                const widgetId = tabs[tabId];

                if (widgetId) {
                    /** set this._widget for {@link _onEvent} */
                    const instance = Help4.widget.getInstance(widgetId);
                    if (instance?.isVisible()) this._widget = widgetId;
                }

                // as edit mode might have changed data - update all widgets
                await Help4.widget.updateAll();

                // reactivate CMP4 handling
                await _onEvent.call(this, {type: TYPES.controllerOpen});
                eventBus.fire({type: TYPES.controllerPlaybackActive, value: true});
            } else if (!readCatalogue && !hasToggle) {
                // restart but w/o reading catalogue and w/o opening immediately; XRAY-6113
                _setHelpAvailable.call(this);
                this._cmp3 = cmp3;
                callback();
                return;
            } else {
                // restart

                startStatus.add(STATUS.done);
                startStatus.rem(STATUS.toggle);

                if (startStatus.has(STATUS.navigate)) {
                    startStatus.rem(STATUS.navigate);

                    const {navScreenId, navStartTour} = __controller._params;
                    __controller.afterNavigate(navScreenId, navStartTour);
                }

                // usually integrate happens in start but not if readCatalogue is false
                await Help4.widget.Infrastructure.integrate();

                // ATTENTION: only set active after integrate!
                this.active = true;

                _monitorWidgetUpdates.call(this);
                _monitorAutoStartTour.call(this);

                _autoStartTour.call(this);
                _setHelpAvailable.call(this);

                const {openImmediately, editor} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration().core;
                const {_open, _minimized} = __controller;
                const {_helpAvailable} = this;
                const shouldOpen = cmp3.openHandler ?? ((editor || _helpAvailable) && openImmediately != null);
                const shouldMinimize = cmp3.openMinimized ?? openImmediately === 'minimized';
                if (_open || hasToggle || shouldOpen) {
                    __controller.open();
                    __controller.onMinimize(shouldMinimize || _minimized);

                    await _activateLastActiveWidget.call(this);
                }

                if (cmp3.reason === 'deserialize') this._widget = tabs[cmp3.carouselTab];

                eventBus.fire({type: TYPES.controllerPlaybackActive, value: true});
            }

            _setPanelVisible.call(this);
            callback();
        }

        /**
         * activates CMP3 mode
         * @param {Object} [params = {}]
         * @param {boolean} [params.terminate = false]
         * @returns {Promise<void>}
         */
        async deactivate({terminate = false} = {}) {
            if (!this.active) return;
            this.active = false;

            this._destroyControl('_statusObserver', '_autoStartTourObserver');
            _setPanelVisible.call(this, false);
            _enableHotkeys.call(this, false);

            const instance = Help4.widget.getActiveInstance();
            const name = instance?.getName();

            // set CMP3 tab to same scope as current widget
            const {EventBus: {TYPES}} = Help4;
            const {__controller} = this;
            const tabs = {
                help: 'help',
                learning: 'learning',
                tourlist: 'tourlist',
                whatsnew: 'wn_help'
            };
            const tabId = tabs[name] || 'help';

            __controller.setContext('carouselTab', tabId);
            __controller.getService('infobar').clean();  // XRAY-2401
            __controller.getService('infobar4').clean();
            __controller.getService('lightbox4').clean();

            // terminate === true: deactivate due to shutdown
            // terminate === false: deactivate due to switch to edit mode
            terminate || __controller.changeHandler(Help4.controller.MODES.helpEdit, {isWhatsNew: tabId === tabs.whatsnew});

            const eventBus = __controller.getService('eventBus');
            eventBus.fire({type: TYPES.controllerPlaybackActive, value: false});

            await instance?.deactivate();
        }

        /** @param {boolean} minimized */
        minimize(minimized) {
            const {__controller, panel} = this;
            __controller.onMinimize(minimized);
            panel.minimized = minimized;
        }
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    function _createPanel() {
        const {Element, control2: {bubble: {Panel}}} = Help4;
        const {__controller, position, docked} = this;

        const {
            core: {editor, isEditorView, rtl, mobile, language, showMinimizeButton, showCloseButton},
            help: {serviceLayer},
            branding: {logoSrc, logoUrl},
            WM
        } = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();

        const dom2Inner = __controller.getDom2('dom');
        if (editor) {
            const dom2Outer = __controller.getDom2();
            Element.addClass(dom2Outer, 'author');
            Element.addClass(dom2Inner, 'author');
        }

        this.panel = /** @type {Help4.control2.bubble.Panel} */ new Panel({
            // logoSrc: 'https://www.sap.com/dam/application/shared/logos/sap-logo-svg.svg/sap-logo-svg.svg',
            brandingLogoSrc: logoSrc,
            brandingLogoUrl: logoUrl,
            dom: dom2Inner,
            docked,
            dragPosition: position,
            showEditButton: editor,
            showPublishViewButton: editor && serviceLayer !== 'uacp',
            showWmButton: WM === 1,
            showMinimizeButton,
            showCloseButton,
            publishView: !isEditorView,
            rtl,
            mobile,
            language: language._,
            contentLanguage: language._,
            visible: false,
            autoFocus: false  // do not steal focus from target app
        })
        .addListener('close', () => WM === 1 ? Help4.WM.closeCMP() : __controller.close())
        .addListener('minimize', ({minimized}) => __controller.onMinimize(minimized))
        .addListener('dragdrop', ({position}) => this.position = position)
        .addListener('edit', () => _onEditClick.call(this))
        .addListener('wm', () => Help4.WM.switchToWM())
        .addListener('dock', ({docked}) => {
            this.docked = docked;
            _setPanelStatus.call(this);
        })
        .addListener('publishView', async () => {
            const {_isEditorView} = __controller;
            const {panel} = this;
            __controller._isEditorView = !_isEditorView;

            panel.publishView = _isEditorView;
            panel.searchTerm = '';

            const activeWidget = Help4.widget.getActiveInstance();
            if (activeWidget?.getName() === 'filter') await activeWidget.deactivate();

            await Help4.widget.redrawAll();
        });
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    function _monitorWidgetUpdates() {
        const {observer: {EventBusObserver}, EventBus: {TYPES}} = Help4;
        const {__controller} = this;
        const eventBus = __controller.getService('eventBus');

        this._statusObserver = new EventBusObserver(event => _onEvent.call(this, event))
        .observe(eventBus, {type: [
            TYPES.widgetStatus,
            TYPES.controllerOpen,
            TYPES.controllerClose,
            TYPES.widgetVisibility,
            TYPES.hotkey
        ]});
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    async function _onEvent({type, data, hotkey}) {
        const {EventBus: {TYPES}, widget} = Help4;
        const {__controller} = this;

        switch (type) {
            // this is just monitored to update panel visibility and panel status below
            case TYPES.widgetStatus:
            case TYPES.widgetVisibility:
                break;

            case TYPES.controllerClose:
                const {core: {readCatalogue}} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();

                if (readCatalogue) {
                    // deactivate widget on controller close
                    _enableHotkeys.call(this, false);

                    const instance = widget.getActiveInstance();
                    await instance?.deactivate();
                } else {
                    let reason = 'bla', carouselTab;
                    const activeWidget = Help4.widget.getActiveInstance()?.getName();
                    if (activeWidget) {
                        const tabs = {
                            help: 'help',
                            learning: 'learning',
                            tourlist: 'tourlist',
                            whatsnew: 'wn_help'
                        };

                        reason = 'deserialize';
                        carouselTab = tabs[activeWidget];
                    }

                    this._cmp3 = {
                        openHandler: false,
                        openMinimized: false,
                        reason,
                        carouselTab
                    }

                    const {STATUS} = Help4.StartStatus;
                    const startStatus = __controller.getService('startStatus');
                    startStatus.rem(STATUS.done);

                    await this.deactivate({terminate: true});
                    await Help4.widget.Infrastructure.terminate();
                }
                break;

            case TYPES.controllerOpen:
                _enableHotkeys.call(this, true);
                await _activateLastActiveWidget.call(this);
                data?.toggle && this.panel.focus();
                break;

            case TYPES.hotkey:
                if (hotkey === 'escape') __controller.getService('message').clean();  // close edit not allowed message
                break;
        }

        _setHelpAvailable.call(this);
        _setPanelVisible.call(this);
        _setPanelStatus.call(this);
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    function _monitorAutoStartTour() {
        const {observer: {EventBusObserver}, EventBus: {TYPES}} = Help4;
        const {__controller} = this;

        const eventBus = __controller.getService('eventBus');
        this._autoStartTourObserver = new EventBusObserver(({alias}) => __controller._autoTourStarted = alias)
        .observe(eventBus, {type: TYPES.autoStartTour});
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    function _autoStartTour() {
        const {__controller} = this;
        const {
            /** @type {?string} */ _autoTourStarted,
            _params: {/** @type {?string} */ autoStartTour}
        } = __controller;

        if (autoStartTour && autoStartTour !== _autoTourStarted) {
            // autostart tour configured and not yet played

            /**
             * control flow:
             * {@link Help4.widget.tour.Widget#setAutoStartTour} - register the <alias>
             * {@link Help4.widget.tour.Widget#_handleAutoStart} - will check for project existence and autostart tour widget
             * {@link Help4.widget.tour.Widget#_onAfterActivate} - start tour playback, if not in conflict with another tour
             */

            const tourInstance = /** @type {?Help4.widget.tour.Widget} */ Help4.widget.getInstance('tour');
            tourInstance?.setAutoStartTour(autoStartTour);  // provide information
        }
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     * @param {?boolean} [force = undefined]
     */
    function _setPanelVisible(force) {
        const {__controller, panel, _helpAvailable} = this;
        const {_open, _minimized, _params: {onHelpMode}} = __controller;
        const {core: {isEditorView, editor, noHelpMode}, WM} = __controller.getConfiguration();
        const instance = Help4.widget.getActiveInstance();

        // XRAY-6023: do not show panel in noHelpMode "nothing" in case no help is available
        const noHelpModeCheck = _helpAvailable || editor || noHelpMode === 'carousel';
        const showPanel = noHelpModeCheck && (instance?.getDescriptor()?.showPanel ?? true);

        panel.minimized = _minimized;
        panel.publishView = !isEditorView;
        panel.visible = WM > 1
            ? false
            : (typeof force === 'boolean'
                ? force
                : _open && showPanel
            );

        // some widgets do not need a panel and therefore no app indentation
        // this can be realized by simulating the CMP3 tour mode that
        // also does not need an app indentation
        onHelpMode(showPanel ? 'help' : 'tour');
    }

    /**
     * update shell information about panel status
     * @memberof Help4.controller.CMP4#
     * @private
     */
    function _setPanelStatus() {
        const {__controller} = this;
        const {CMP4, isEditMode, isRemoteMode} = __controller.getConfiguration();
        if (CMP4 && !isEditMode && !isRemoteMode) {
            const {_params: {onHelpCarousel}, _open} = __controller;
            onHelpCarousel(this.docked && _open);
        }
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     * @param {boolean} enable
     */
    function _enableHotkeys(enable) {
        const {HOTKEY} = Help4;
        const {__controller} = this;
        const hotkey = __controller.getService('hotkey');
        const list = [
            HOTKEY.focusApp, HOTKEY.focusHelp4,
            HOTKEY.escape, HOTKEY.space, HOTKEY.enter,
            HOTKEY.prevListItem, HOTKEY.nextListItem,
            HOTKEY.leftListItem, HOTKEY.rightListItem
        ];

        enable
            ? hotkey.enableHotkey(...list)
            : hotkey.disableHotkey(...list);
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     * @returns {Promise<void>}
     */
    async function _activateLastActiveWidget() {
        const {_widget} = this;
        this._widget = null;

        const instance = /** @type {?Help4.widget.Widget} */ _widget && Help4.widget.getInstance(_widget);
        await instance?.activate();
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     * @param {Object} status
     * @returns {?Object}
     */
    function _getLastActiveWidget(status) {
        // find the last active widget from status
        return Object.values(status).find(({active}) => active);
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    function _setHelpAvailable() {
        const {__controller} = this;
        const {core: {editor, noHelpMode}} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();

        let available = false;
        for (/** @type {Help4.widget.Widget} */ const widget of Help4.widget) {
            if (widget instanceof Help4.widget.filter.Widget) continue;
            available ||= widget.getDescriptor().showPanel && widget.isVisible();
        }
        this._helpAvailable = available;

        __controller.onHelpAvailable(available || editor || noHelpMode !== 'hidebutton');
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     */
    async function _onEditClick() {
        const {__controller} = this;
        const {core: {languageFallbackMode, screenId}, help: {serviceLayer, roModel}}= /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
        const {ext} = Help4.SERVICE_LAYER;

        if (languageFallbackMode === 'mix' && serviceLayer === ext) {
            let sameLanguage = true;
            const helpWidget = /** @type {?Help4.widget.help.Widget} */ Help4.widget.getInstance('help');
            if (helpWidget) sameLanguage = _areSameLanguageProjects.call(this, helpWidget, screenId, roModel);

            if (sameLanguage) {
                const whatsnewWidget = /** @type {?Help4.widget.help.Widget} */ Help4.widget.getInstance('whatsnew');
                const {WHATSNEW_SCREEN_ID} = Help4.widget.help.CatalogueBackend;
                if (whatsnewWidget) sameLanguage = _areSameLanguageProjects.call(this, whatsnewWidget, screenId + WHATSNEW_SCREEN_ID, roModel);
            }

            if (!sameLanguage) {
                const {Localization, control2: {ICONS}} = Help4;
                __controller.getService('message').add({
                    icon: ICONS.info,
                    caption: Localization.getText('header.editimpossible'),
                    content: Localization.getText('label.editimpossible.notsamelanguage'),
                    primaryButton: 'ok',
                    buttons: ['ok'],
                });
                return;
            }
        }

        this.deactivate();
    }

    /**
     * @memberof Help4.controller.CMP4#
     * @private
     * @param {Help4.widget.Widget} widget
     * @param {string} screenId
     * @param {string} roModel
     * @return {boolean}
     */
    function _areSameLanguageProjects(widget, screenId, roModel) {
        const {uacp, wpb, sen} = Help4.SERVICE_LAYER;
        const {widget: {help: {data}}} = widget.getContext();
        const catalogueType = (roModel === wpb ? sen : uacp).toUpperCase();
        const projects = data.getProjects(screenId, catalogueType, 'head');  // head is always editable content
        if (projects.length > 1) {
            const [{language: roLang}, {language: rwLang}] = projects;
            const rwLangCodes = Help4.LOCALE_MAP.filter(langObj => langObj.uacp === roLang).map(langObj => langObj.wpb);
            return Help4.includes(rwLangCodes, rwLang);
        }

        return true;
    }
})();