Source: control2/bubble/Panel.js

(function() {
    /**
     * @typedef {Help4.control2.bubble.Bubble.Params} Help4.control2.bubble.Panel.Params
     * @property {?string} [logoSrc = null] - URL to possible logo
     * @property {boolean} [showBackButton = false] - whether to show back button
     * @property {boolean} [showDockButton = true] - whether to show dock button
     * @property {boolean} [showMinimizeButton = true] - whether to show minimize button
     * @property {boolean} [showCloseButton = true] - whether to show close button
     * @property {boolean} [docked = false] - whether panel is docked
     * @property {boolean} [minimized = false] - whether panel is minimized
     * @property {boolean} [showTranslationButton = false] - whether to show the translation button
     * @property {boolean} [translation = false] - whether translation is enabled
     * @property {boolean} [animateTranslationButton = false] - whether to animate the translation button
     * @property {?string} [caption = null] - caption text
     * @property {boolean} [showCaption = true] - caption text
     * @property {boolean} [showSearch = true] - whether to show search
     * @property {boolean} [showTiles = true] - whether to show the widget tiles
     * @property {boolean} [showContentArea = false] - whether to show the content area
     * @property {?string} [brandingLogoSrc = null] - image source URL to possible branding logo
     * @property {?string} [brandingLogoUrl = null] - navigation link to possible branding logo
     * @property {boolean} [showEditButton = false] - whether to show the edit button
     * @property {boolean} [showPublishViewButton = false] - whether to show the publish view button
     * @property {boolean} [showWmButton = false] - whether to show the wm button
     * @property {boolean} [publishView = false] - whether publish view is enabled
     * @property {?string} [publishedState = null] - published state of the widget content
     */

    /**
     * Creates the bubble for the SAP Companion 4 panel.
     * @augments Help4.control2.bubble.Bubble
     * @property {?string} logoSrc - URL to possible logo
     * @property {boolean} showBackButton - whether to show back button
     * @property {boolean} showDockButton - whether to show dock button
     * @property {boolean} showMinimizeButton - whether to show minimize button
     * @property {boolean} showCloseButton - whether to show close button
     * @property {boolean} showTranslationButton - whether to show the translation button
     * @property {boolean} translation - whether translation is enabled
     * @property {boolean} animateTranslationButton - whether to animate the translation button
     * @property {boolean} docked - whether panel is docked
     * @property {boolean} minimized - whether panel is minimized
     * @property {?string} caption - caption text
     * @property {boolean} showCaption - caption text
     * @property {boolean} showTiles - whether to show the widget tiles
     * @property {boolean} showContentArea - whether to show the content area
     * @property {boolean} showSearch - whether to show search
     * @property {string} searchTerm - the search term
     * @property {?string} brandingLogoSrc - image source URL to possible branding logo
     * @property {?string} brandingLogoUrl - navigation link to possible branding logo
     * @property {boolean} showEditButton - whether to show the edit button
     * @property {boolean} showPublishViewButton - whether to show the publish view button
     * @property {boolean} showWmButton - whether to show the wm button
     * @property {boolean} publishView - whether publish view is enabled
     * @property {?string} publishedState - published state of the widget content
     */
    Help4.control2.bubble.Panel = class extends Help4.control2.bubble.Bubble {
        /**
         * @override
         * @param {Help4.control2.bubble.Panel.Params} params
         */
        constructor(params) {
            const {Localization} = Help4;
            const caption = Localization.getText('header.panel.howcan');
            const ariaLabel = Localization.getText('description.panel');
            const ariaRoleDescription = Localization.getText('description.panel.label');

            const {TYPES: T} = Help4.jscore.ControlBase;
            const {
                HEADER_LAYOUT,
                CONTENT_LAYOUT,
                FOOTER_LAYOUT
            } = Help4.control2.bubble;

            super(params, {
                params: {
                    // header; keep in sync with bubble.header.Panel!
                    logoSrc:                  {type: T.string_null},
                    showBackButton:           {type: T.boolean},
                    showDockButton:           {type: T.boolean, init: true},
                    showMinimizeButton:       {type: T.boolean, init: true},
                    showCloseButton:          {type: T.boolean, init: true},
                    docked:                   {type: T.boolean},
                    minimized:                {type: T.boolean},
                    translation:              {type: T.boolean},
                    showTranslationButton:    {type: T.boolean},
                    animateTranslationButton: {type: T.boolean},

                    // content; keep in sync with bubble.content.Panel!
                    caption:         {type: T.string_null, init: caption},
                    showCaption:     {type: T.boolean, init: true},
                    showTiles:       {type: T.boolean, init: true},
                    showContentArea: {type: T.boolean},
                    showSearch:      {type: T.boolean, init: true},
                    searchTerm:      {type: T.string},
                    brandingLogoSrc: {type: T.string_null, readonly: true},
                    brandingLogoUrl: {type: T.string_null, readonly: true},

                    // footer; keep in sync with bubble.footer.Panel!
                    publishView:           {type: T.boolean},
                    showPublishViewButton: {type: T.boolean},
                    showWmButton:          {type: T.boolean},
                    showEditButton:        {type: T.boolean},
                    publishedState:        {type: T.string_null},

                    // own properties
                    size:                {init: 'panel'},
                    role:                {init: 'application'},
                    ariaLabel:           {init: ariaLabel},
                    ariaRoleDescription: {init: ariaRoleDescription},
                    headerLayout:        {init: HEADER_LAYOUT.Panel},
                    contentLayout:       {init: CONTENT_LAYOUT.Panel},
                    footerLayout:        {init: FOOTER_LAYOUT.Panel},
                    enableDragDrop:      {init: true}
                },
                statics: {
                    _minimizeButton:       {},
                    _widgetEngineObserver: {},
                    _windowObserver:       {},
                    _activeWidgetName:     {destroy: false},
                    _beforeActivate:       {destroy: false}
                },
                config: {
                    css: 'control-panel home noexternal'
                }
            });
        }

        /**
         * @override
         * @param {boolean} [onlyVisible = false]
         */
        getTexts(onlyVisible = false) {
            const {_minimizeButton} = this;
            return _minimizeButton
                ? _minimizeButton.getTexts(onlyVisible)
                : super.getTexts(onlyVisible);
        }

        /**
         * @override
         * @param {Object} textMap
         */
        setTexts(textMap) {
            const {_minimizeButton} = this;
            return _minimizeButton
                ? _minimizeButton.setTexts(textMap)
                : super.setTexts(textMap);
        }

        /**
         * @param {number} left
         * @param {number} top
         */
        setPosition(left, top) {
            this.dataFunctions.set({dragPosition: {left, top}}, {allowReadonlyOverride: true});  // this.dragPosition is readonly; allow override

            this.setStyle({
                left: `${left}px`,
                top: `${top}px`
            });

            _onResize.call(this);
        }

        /**
         * @override
         * @returns {Help4.control2.bubble.Panel}
         */
        focus() {
            this.minimized = false;

            Help4.Element.execAfterTransition(this.getDom(), () => {
                if (!this.isDestroyed()) this._content?.focus();
            });

            return this;
        }

        /**
         * focus handling - up/down/left/right arrow keys
         * @param {string} direction
         */
        focusListItem(direction) {
            this._content?.focusListItem(direction);
        }

        /**
         * @override
         * @param {HTMLElement} dom - control DOM
         */
        _onDomAvailable(dom) {
            super._onDomAvailable(dom);

            if (this.minimized && this.visible) {
                // in case panel is visible and minimized it will flicker during creation
                // as it moves out but is immediately hidden through minimize
                this.addCss('hidden');  // add hidden class before adding the panel to the DOM
            }

            const eventBus = /** @type {Help4.EventBus} */ Help4.getController().getService('eventBus');
            const {
                observer: {EventBusObserver, WindowObserver},
                EventBus: {TYPES}
            } = Help4;

            this._widgetEngineObserver = new EventBusObserver(event => _onWidgetEvent.call(this, event))
            .observe(eventBus, {type: [TYPES.widgetStart, TYPES.widgetActivate]});

            this._windowObserver = new WindowObserver(event => _onResize.call(this))
            .observe(window, {eventObserver: {type: 'resize', capture: true}});
        }

        /** @override */
        _onGetDragDropParams() {
            return {
                object: this.getDom(),
                area: this._header.getDom()
            };
        }

        /**
         * incompatible override!
         * @protected
         */
        _onCreateHeader() {
            const {showTranslationButton, translation, animateTranslationButton, showMinimizeButton, showCloseButton} = this;
            super._onCreateHeader({showTranslationButton, translation, animateTranslationButton, showMinimizeButton, showCloseButton});
            this._header?.addListener(['back', 'dock', 'minimize', 'close', 'translate'], event => void _onEvent.call(this, event));
        }

        /**
         * incompatible override!
         * @protected
         */
        _onCreateContent() {
            const {brandingLogoSrc, brandingLogoUrl} = this;
            super._onCreateContent({brandingLogoSrc, brandingLogoUrl});
            this._content?.addListener(['widget', 'search'], event => void _onEvent.call(this, event));
            this._content?.dataFunctions.onChange.addListener(({name, value}) => (name === 'searchTerm') && (this.searchTerm = value));
        }

        /**
         * incompatible override!
         * @protected
         */
        _onCreateFooter() {
            const {showEditButton, showPublishViewButton, publishView, showWmButton} = this;
            super._onCreateFooter({showEditButton, showPublishViewButton, publishView, showWmButton});
            this._footer?.addListener(['edit', 'publishView', 'wm'], event => void _onEvent.call(this, event));
        }

        /**
         * @override
         * @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
         */
        _applyPropertyToDom({name, value, oldValue}) {
            switch (name) {
                // header
                case 'logoSrc':
                case 'showBackButton':
                case 'showDockButton':
                case 'showMinimizeButton':
                case 'showCloseButton':
                case 'translation':
                case 'showTranslationButton':
                case 'animateTranslationButton':
                    this._header[name] = value;
                    break;
                case 'docked':
                    const {Localization} = Help4;
                    this._header.docked = value;

                    if (value) {
                        this.suspendDragDrop();
                        this.removeCss('floating');
                        this.addCss('docked');
                        this.ariaRoleDescription = Localization.getText('description.carousel.label');
                    } else {
                        this.removeCss('docked');
                        this.addCss('floating');
                        this.resumeDragDrop();
                        this.ariaRoleDescription = Localization.getText('description.panel.label');
                    }
                    _adjustPosition.call(this);
                    break;
                case 'minimized':
                    this._header.minimized = value;
                    _minimize.call(this, value);
                    _adjustPosition.call(this);
                    break;

                // content
                case 'searchTerm':
                    const lower = value.toLowerCase();
                    if (value === lower) {
                        const filterWidget = /** @type {Help4.widget.filter.Widget} */ Help4.widget.getInstance('filter');
                        this._content[name] = value;
                        filterWidget?.setTerm(value);  // synchronize with filter widget
                    } else {
                        // reset with lowercase
                        this.searchTerm = lower;
                    }
                    break;
                case 'caption':
                case 'showCaption':
                case 'showSearch':
                case 'showTiles':
                case 'showContentArea':
                    this._content[name] = value;
                    break;

                // footer
                case 'publishView':
                case 'showPublishViewButton':
                case 'publishedState':
                case 'showEditButton':
                case 'showWmButton':
                    this._footer[name] = value;
                    break;

                // own properties
                case 'visible':
                    const {_minimizeButton} = this;
                    if (_minimizeButton) {
                        _minimizeButton.visible = value;
                    } else {
                        super._applyPropertyToDom({name, value, oldValue});
                    }
                    _adjustPosition.call(this);
                    break;

                default:
                    super._applyPropertyToDom({name, value, oldValue});
                    break;
            }
        }
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @param {Object} event
     * @private
     */
    function _onEvent(event) {
        switch (event.type) {
            case 'back':
                if (Help4.widget.getActiveInstance()?.getName() === 'filter') {
                    this.searchTerm = '';
                }

                const monitor = /** @type {Help4.widget.monitor.Widget} */ Help4.widget.getInstance('monitor');
                monitor?.executeBack() || _deactivateWidget.call(this);
                break;
            case 'dock':
                event.docked = this.docked = !this.docked;
                this._fireEvent(event);
                break;
            case 'minimize':
                this.minimized = !this.minimized;
                break;
            case 'publishView':
            case 'translate':
            case 'close':
            case 'edit':
            case 'wm':
                this._fireEvent(event);
                break;
            case 'search':
                const filterWidget = /** @type {Help4.widget.filter.Widget} */ Help4.widget.getInstance('filter');
                filterWidget?.execSearch(this.searchTerm);
                break;
            case 'widget':
                _activateWidget.call(this, event.id);
                break;
        }
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @param {boolean} minimize
     * @private
     */
    function _minimize(minimize) {
        const {_minimizeButton} = this;

        if (minimize && !_minimizeButton) {
            // hide the panel w/o changing this.visible
            // instead of the panel the minimize button will be shown
            this._applyPropertyToDom({name: 'visible', value: false});
            this.addCss('minimize');

            const {Button, APPEARANCES} = Help4.control2.button;
            const title = Help4.Localization.getText('tooltip.carouselhide');
            const {visible} = this;

            this._minimizeButton = this._createControl(Button, {
                doc: this.getDocument(),
                dom: this.getOwnerDom(),
                appearance: APPEARANCES.icon,
                icon: Help4.control2.ICONS.help,
                css: 'sap-companion',
                title,
                ariaLabel: title,
                visible
            })
            .addListener('click', () => {
                this.minimized = false;
                this.focus();
            });
        }

        if (!minimize && _minimizeButton) {
            // take visible state from _minimizeButton as panel state has not been updated
            // while minimize button was shown
            const visible = _minimizeButton.visible;
            this._destroyControl('_minimizeButton');
            this.removeCss('minimize');

            if (this.visible === visible) {
                // make sure to apply to DOM; show panel as button is now hidden
                this._applyPropertyToDom({name: 'visible', value: visible});
            } else {
                this.visible = visible;  // take over the new visibility
            }
        }

        this._fireEvent({type: 'minimize', minimized: minimize});
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @param {string} name
     * @private
     */
    function _activateWidget(name) {
        Help4.widget.getInstance(name)?.activate();
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @private
     */
    function _deactivateWidget() {
        Help4.widget.getInstance(this._activeWidgetName)?.deactivate();
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @param {Help4.widget.Widget.Event} event
     * @private
     */
    function _onWidgetEvent({type, engine, value}) {
        const {TYPES} = Help4.EventBus;
        switch (type) {
            case TYPES.widgetStart:
                if (!value) _deactivateWidget.call(this);
                break;
            case TYPES.widgetActivate:
                if (value) {
                    this.removeCss('home');

                    const {caption: c, logoSrc: l} = this;
                    this._beforeActivate = {c, l};

                    const {id, tile} = engine.getDescriptor();
                    this._activeWidgetName = id;
                    this.logoSrc = null;
                    this.showBackButton = true;
                    this.showSearch = tile?.showSearch || false;
                    this.showTiles = false;
                    this.caption = tile?.title || '';
                    this.showContentArea = true;
                } else if (this._beforeActivate) {
                    const {c: caption, l: logoSrc} = this._beforeActivate;
                    delete this._beforeActivate;

                    this._activeWidgetName = null;
                    this.logoSrc = logoSrc;
                    this.showBackButton = false;
                    this.showContentArea = false;
                    this.showTiles = true;
                    this.showSearch = true;
                    this.caption = caption;

                    this.addCss('home');
                }
                break;
        }
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @private
     */
    function _adjustPosition() {
        const dom = this.getDom();
        Help4.Element.execAfterTransition(dom, () => {
            if (!_isPanelResizable.call(this)) return;

            // get panel w,h from CSS variables
            const style = getComputedStyle(dom);
            const w = parseInt(style.getPropertyValue('--help4-cmp-pane-w'));
            const h = parseInt(style.getPropertyValue('--help4-cmp-pane-h'));

            // get window size and last drag position
            const {innerWidth, innerHeight} = window;
            const {left, top} = this.dragPosition || {};

            // check whether drag position is still valid
            if (isNaN(left) || left + w > innerWidth || top + h > innerHeight) {
                // invalid: recalculate
                _onResize.call(this);
            } else {
                // valid: simply apply
                this.setStyle({
                    left: `${left}px`,
                    top: `${top}px`
                });
            }
        });
    }

    /**
     * @memberof Help4.control2.bubble.Panel#
     * @private
     */
    function _onResize() {
        if (!_isPanelResizable.call(this)) return;

        // get size of window
        const {innerWidth, innerHeight} = window;

        // calculate min, max positions for X and Y
        const {x, y} = this.getArea();

        // get panel w, h from CSS variables
        const style = getComputedStyle(this.getDom());
        const w = parseInt(style.getPropertyValue('--help4-cmp-pane-w'));
        const h = parseInt(style.getPropertyValue('--help4-cmp-pane-h'));

        const minX = 0;
        const minY = 0;
        const maxX = Math.max(minX, innerWidth - w);
        const maxY = Math.max(minY, innerHeight - h);

        // adjust panel position
        const newX = Math.max(minX, Math.min(x, maxX));
        const newY = Math.max(minY, Math.min(y, maxY));

        // apply
        this.setStyle({
            left: `${newX}px`,
            top: `${newY}px`
        });

        // notify watchers
        this._onDragStop();
    }

    /**
     * only allow position adjustments and resize for a floating, visible panel
     * @memberof Help4.control2.bubble.Panel#
     * @private
     * @returns {boolean}
     */
    function _isPanelResizable() {
        const {docked, minimized, visible} = this;
        return !docked && !minimized && visible && !this.isDestroyed();
    }
})();