Source: control2/bubble/content/Panel.js

(function() {
    /**
     * @typedef {Help4.control2.bubble.content.BubbleContent.Params} Help4.control2.bubble.content.Panel.Params
     * @property {?string} [caption = null] - caption
     * @property {boolean} [showCaption = false] - whether to show caption
     * @property {boolean} [showSearch = false] - whether to show search
     * @property {boolean} [showTiles = false] - 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
     */

    /**
     * SAP Companion 4 panel content.
     * @augments Help4.control2.bubble.content.BubbleContent
     * @property {?string} caption - caption
     * @property {boolean} showCaption - whether to show caption
     * @property {boolean} showSearch - whether to show search
     * @property {string} searchTerm - the search term
     * @property {boolean} showTiles - whether to show the widget tiles
     * @property {boolean} showContentArea - whether to show the content area
     * @property {?string} brandingLogoSrc - image source URL to possible branding logo
     * @property {?string} brandingLogoUrl - navigation link to possible branding logo
     * @property {string} __usedContentArea - defines what content area is used
     * @property {?Help4.control2.Text} _caption
     * @property {?Help4.control2.container.PanelSearch} _panelSearch
     * @property {?Help4.control2.container.PanelWidgetContainer} _panelWidgetContainer
     * @property {?HTMLDivElement} _contentDiv
     * @property {?HTMLIFrameElement} _sameOriginContent
     * @property {?HTMLIFrameElement} _crossOriginContent
     */
    Help4.control2.bubble.content.Panel = class extends Help4.control2.bubble.content.BubbleContent {
        /**
         * @override
         * @param {Help4.control2.bubble.content.Panel.Params} [params]
         */
        constructor(params) {
            const {
                TYPES: T,
                TEXT_TYPES: TT
            } = Help4.jscore.ControlBase;

            super(params, {
                params: {
                    // all defaults are handled by bubble.Panel; do not handle here!
                    // keep in sync with bubble.Panel!
                    caption:         {type: T.string_null},
                    showCaption:     {type: T.boolean},
                    showSearch:      {type: T.boolean},
                    searchTerm:      {type: T.string},
                    showTiles:       {type: T.boolean},
                    showContentArea: {type: T.boolean},
                    brandingLogoSrc: {type: T.string_null, readonly: true},
                    brandingLogoUrl: {type: T.string_null, readonly: true},
                    role:            {init: 'main'},

                    usedContentArea: {type: T.string, private: true}
                },
                statics: {
                    _caption:              {},
                    _panelSearch:          {},
                    _panelWidgetContainer: {},
                    _contentDiv:           {destroy: false},
                    _sameOriginContent:    {destroy: false},
                    _crossOriginContent:   {destroy: false}
                },
                config: {
                    css: 'control-bubble-content-panel'
                },
                texts: {
                    brandingLogoTitle: TT.title,
                    brandingLogoAlt: TT.alt
                }
            });
        }

        /**
         * @returns {HTMLDivElement}
         */
        useContentDiv() {
            this.__usedContentArea = 'contentDiv';
            return this._contentDiv;
        }

        /**
         * @returns {HTMLIFrameElement}
         */
        useInternalIframe() {
            this.__usedContentArea = 'internalIframe';
            return this._sameOriginContent;
        }

        /**
         * @returns {HTMLIFrameElement}
         */
        useExternalIframe() {
            this.__usedContentArea = 'externalIframe';
            return this._crossOriginContent;
        }

        /**
         * @override
         * @returns {Help4.control2.bubble.content.Panel}
         */
        focus() {
            const {showSearch, _panelSearch, showTiles, _panelWidgetContainer} = this;

            showSearch
                ? _panelSearch.focus()  // focus on search bar input
                : showTiles && !!_panelWidgetContainer?.count()
                    ? _panelWidgetContainer.get(0).focus()  // we are in home screen and focus the first widget tile, if available
                    : super.focus();  // focus content area instead

            return this;
        }

        /**
         * focus handling - up/down/left/right arrow keys
         * @param {string} direction
         */
        focusListItem(direction) {
            const {showTiles, _panelWidgetContainer} = this;
            const count = _panelWidgetContainer?.count() || 0;

            if (!showTiles || count <= 1) return;

            const visibleWidgets = [];
            _panelWidgetContainer.forEach(widgetControl => widgetControl.visible && visibleWidgets.push(widgetControl));

            const focussedElement = /** @type {HTMLElement} */ Help4.widget.getActiveElement();
            let index = visibleWidgets.findIndex(widgetControl => widgetControl.getDom() === focussedElement);

            if (index < 0) return;  // focussed control is not a widget

            const {x, left} = focussedElement.getBoundingClientRect();

            switch (direction) {
                case 'up':
                    while(index > 0) {
                        const prevControl = visibleWidgets[index - 1];
                        const {x: px, left: pleft} = prevControl.getDom().getBoundingClientRect();
                        if (x === px && left === pleft) {
                            prevControl.focus();
                            break;
                        } else {
                            index--;
                        }
                    }
                    break;
                case 'down':
                    while(index < count - 1) {
                        const nextControl = visibleWidgets[index + 1];
                        const {x: nx, left: nleft} = nextControl.getDom().getBoundingClientRect();
                        if (x === nx && left === nleft) {
                            nextControl.focus();
                            break;
                        } else {
                            index++;
                        }
                    }
                    break;
                case 'left':
                    visibleWidgets[index - 1]?.focus();
                    break;
                case 'right':
                    visibleWidgets[index + 1]?.focus();
                    break;
            }
        }

        /**
         * @override
         * @param {Help4.control2.bubble.content.Panel.Params} params - same params as provided to the constructor
         */
        _onAfterInit(params) {
            super._onAfterInit(params);

            // remove "dgo_text_container" class as not applicable for panel
            // this.css is readonly and cannot be changed
            // therefore use specific readonly override handling from Help4.jscore.ControlBase
            this.dataFunctions.set({css: this.css.replace(/dgo_text_container/, '')}, {allowReadonlyOverride: true});
        }

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

            const {id, __bubble, brandingLogoSrc, brandingLogoUrl} = this;
            const {control2, Element, Localization} = Help4;

            this._caption = this._createControl(control2.Text, {
                text: this.caption,
                id: `${id}-caption`,
                dom,
                tag: 'h3',
                css: 'caption'
            });

            const ps = this._panelSearch = this._createControl(control2.container.PanelSearch, {
                id: `${id}-panelsearch`,
                dom
            })
            .addListener('search', event => void this._fireEvent(event));

            ps.dataFunctions.onChange.addListener(({name, value}) => (name === 'value') && (this.searchTerm = value));

            this._panelWidgetContainer = this._createControl(control2.container.PanelWidgetContainer, {
                id: `${id}-panelwidgets`,
                dom,
                panel: __bubble,
                role: 'list',
                ariaRoleDescription: Localization.getText('description.widget.list')
            })
            .addListener('widget', event => void this._fireEvent(event));

            const document = this.getDocument();

            if (brandingLogoSrc && brandingLogoUrl) {
                const title = Localization.getText('tooltip.carousel.logourl');
                const brandingLink = Element.create('A', {
                    id: `${id}-branding-logo-anchor`,
                    dom,
                    document,
                    css: 'logo-link',
                    href: brandingLogoUrl,
                    title,
                    ariaLabel: title,
                    onclick: event => {
                        event.preventDefault();  // prevented navigation to securely open the link in a new window
                        Help4.windowOpen(brandingLogoUrl);
                    }
                });
                this._setTextAttribute(brandingLink, 'brandingLogoTitle');

                const brandingImg = Element.create('IMG', {
                    id: `${id}-branding-logo-image`,
                    dom: brandingLink,
                    document,
                    css: 'logo-image',
                    src: brandingLogoSrc,
                    role: 'presentation',
                    alt: Localization.getText('tooltip.carousel.logosrc')
                });
                this._setTextAttribute(brandingImg, 'brandingLogoAlt');
            }

            this._contentDiv = Element.create('DIV', {
                dom,
                document,
                css: 'widget-content hidden'
            });

            this._sameOriginContent = Element.create('IFRAME', {
                dom,
                document,
                css: 'widget-content same-origin hidden'
            });

            this._crossOriginContent = Element.create('IFRAME', {
                dom,
                document,
                css: 'widget-content cross-origin hidden'
            });
        }

        /**
         * @override
         * @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
         */
        _applyPropertyToDom({name, value, oldValue}) {
            switch (name) {
                case 'caption':
                    this._caption.text = value;
                    break;
                case 'showCaption':
                    this._caption.visible = value;
                    break;
                case 'searchTerm':
                    this._panelSearch.value = value;
                    break;
                case 'showSearch':
                    this._panelSearch.visible = value;
                    break;
                case 'showTiles':
                case 'showContentArea':
                    _handleVisibility.call(this, name);
                    break;
                case 'usedContentArea':
                    _handleVisibility.call(this, 'showContentArea');
                    break;
                default:
                    super._applyPropertyToDom({name, value, oldValue});
                    break;
            }
        }
    }

    /**
     * @memberof Help4.control2.bubble.content.Panel#
     * @param {'showTiles'|'showContentArea'} propertyName
     * @private
     */
    function _handleVisibility(propertyName) {
        const controls = {
            tiles: this._panelWidgetContainer,
            contentDiv: this._contentDiv,
            internalIframe: this._sameOriginContent,
            externalIframe: this._crossOriginContent
        };

        const visible = this[propertyName];
        const {__bubble, __usedContentArea} = this;

        const show = (elem, visible) => {
            elem instanceof Help4.control2.Control
                ? elem.visible = visible
                : Help4.Element[visible ? 'removeClass' : 'addClass'](elem, 'hidden');  // DOM Node
        }

        if (propertyName === 'showTiles') {
            // my properties are just mirroring the ones of my bubble
            // update the bubble instead of myself;
            // bubble will propagate to me if needed
            if (visible) __bubble.showContentArea = false;
            show(controls.tiles, visible);
        } else {  // showContentArea
            if (visible && __usedContentArea) __bubble.showTiles = false;
            for (const key of ['contentDiv', 'internalIframe', 'externalIframe']) {
                show (controls[key], visible && __usedContentArea === key);
            }
        }
    }
})();