Source: widget/static.js

(function() {
    /**
     * @typedef {Function} Help4.widget.AwaitAllExecutor
     * @param {Help4.widget.Widget} instance
     * @returns {Promise<*>}
     */

    /**
     * color definition; values to-be-defined in LESS
     * @enum {string}
     */
    Help4.widget.COLOR = {
        color1: 'color1',
        color2: 'color2',
        color3: 'color3',
        color4: 'color4',
        color5: 'color5',
        color6: 'color6',
        color7: 'color7',
        color8: 'color8'
    };

    /** @enum {string} */
    Help4.widget.POSITION = {
        panel: 'panel',
        lightbox: 'lightbox',
        tab: 'tab'
    };

    /** @type {Help4.widget.Widget[]} */
    Help4.widget.INSTANCES = [];

    /** @type {{params: Help4.tracking.Tracking.TrackParams, object: Object}} */
    Help4.widget.TRACK_STATUS = {params: {}, object: {}};

    /**
     * throws an error that an instance of the same widget already exists
     * @param {string} name - widget namespace
     * @throws {Error}
     */
    Help4.widget.assertSingleton = (name) => {
        if (Help4.widget.getInstance(name)) {
            throw new Error(`Only one instance of "Help4.widget.${name}" allowed!`);
        }
    }

    /**
     * creates a widget of a certain namespace
     * @param {string} name - widget namespace
     * @returns {Help4.widget.Widget}
     * @throws {Error}
     */
    Help4.widget.create = (name) => {
        const widgetClass = Help4.widget[name]?.Widget;
        if (widgetClass) return new widgetClass();
        throw new Error(`Invalid namespace "Help4.widget.${name}" for widget creation!`);
    }

    /**
     * get the widget instance of a certain namespace
     * @param {string} names - widget namespace(s)
     * @returns {Help4.widget.Widget|null|Object}
     */
    Help4.widget.getInstance = (...names) => {
        const {INSTANCES} = Help4.widget;

        const get = name => {
            for (/** @type {Help4.widget.Widget} */ const instance of INSTANCES) {
                if (instance.getName() === name) return instance;
            }
            return null;
        }

        if (!names.length) {
            const instances = {};
            for (/** @type {Help4.widget.Widget} */ const instance of INSTANCES) {
                const name = instance.getName();
                instances[name] = instance;
            }
            return instances;
        }

        if (names.length === 1) {
            return get(names[0]);
        }

        const instances = {};
        for (const name of names) {
            instances[name] = get(name);
        }
        return instances;
    }

    /**
     * used to retrieve a list of all visible texts for all widgets
     * @returns {Promise<Object>}
     */
    Help4.widget.getTexts = async () => {
        const texts = await Help4.widget.awaitAll(instance => instance.getTexts());

        if (!Help4.widget.getActiveInstance()) {
            const controller = /** @type {?Help4.controller.Controller} */ Help4.getController();
            if (controller?.isOpen()) {
                const cmp4 = /** @type {?Help4.controller.CMP4} */ controller.getCmp4Handler();
                const {/** @type {?Help4.control2.bubble.Panel} */ panel} = cmp4 || {};
                if (panel?.visible) texts._panel = panel.getTexts();
            }
        }

        return texts;
    }

    /**
     * used to replace all visible texts for all widgets
     * @param {Object} texts
     * @returns {Promise<void>}
     */
    Help4.widget.setTexts = async (texts) => {
        if (texts._panel && !Help4.widget.getActiveInstance()) {
            const controller = /** @type {?Help4.controller.Controller} */ Help4.getController();
            if (controller?.isOpen()) {
                const cmp4 = /** @type {?Help4.controller.CMP4} */ controller.getCmp4Handler();
                const {/** @type {?Help4.control2.bubble.Panel} */ panel} = cmp4 || {};
                if (panel?.visible) panel.setTexts(texts._panel);
            }
        }
        delete texts._panel;

        await Help4.widget.awaitAll(instance => {
            const name = instance.getName();
            return texts[name] ? instance.setTexts(texts[name]) : null;
        });
    }

    /**
     * delivers the active widget instance
     * @returns {?Help4.widget.Widget}
     */
    Help4.widget.getActiveInstance = () => {
        const {INSTANCES} = Help4.widget;
        /** @type {Help4.widget.Widget} */
        for (const instance of INSTANCES) {
            if (instance.isActive()) return instance;
        }
        return null;
    }

    /**
     * updates all existing widgets
     * @returns {Promise<void>}
     */
    Help4.widget.updateAll = async () => {
        await Help4.widget.awaitAll(instance => instance.updateData());
    }

    /**
     * redraws all existing widgets
     * @returns {Promise<void>}
     */
    Help4.widget.redrawAll = async () => {
        await Help4.widget.awaitAll(instance => instance.redraw());
    }

    /**
     * @param {Help4.widget.AwaitAllExecutor} executor
     * @param {'parallel'|'sequential'} [mode = 'parallel']
     * @returns {Promise<Object>}
     */
    Help4.widget.awaitAll = async (executor, mode = 'parallel') => {
        const map = {};

        if (mode === 'parallel') {
            // parallel mode: start-start-start-start-...-wait
            /**  @type {Array<Promise<*>>} */ const promises = [];
            /**  @type {string[]} */ const names = [];

            Help4.widget.forEach(
                /** @param {Help4.widget.Widget} instance */
                (instance) => {
                    promises.push(executor(instance));
                    names.push(instance.getName());
                }
            );

            const results = await Help4.Promise.all(promises);
            names.forEach((name, index) => map[name] = results[index]);
        } else {
            // sequential mode: start-wait-start-wait-...
            const {INSTANCES} = Help4.widget;
            for (const instance of INSTANCES) {
                map[instance.getName()] = await executor(instance);
            }
        }

        return map;
    }

    /**
     * Help4.widget.forEach(widgetInstance => ...)
     *
     * @param {Help4.typedef.ForEachExecutor} executor
     */
    Help4.widget.forEach = (executor) => {
        const {INSTANCES} = Help4.widget;
        for (const [index, instance] of Help4.arrayEntries(INSTANCES)) {
            executor(instance, index, INSTANCES);
        }
    }

    /**
     * for (const [index, widgetInstance] of Help4.widget.entries()) {
     *     ...
     * }
     *
     * @generator
     * @yields {Array<number, Help4.widget.Widget>}
     */
    Help4.widget.entries = function*() {
        const {INSTANCES} = Help4.widget;
        let index = 0;

        while (INSTANCES[index]) {
            yield [index, INSTANCES[index]];
            index++;
        }
    }

    /**
     * for (const widgetInstance of Help4.widget) {
     *     ...
     * }
     *
     * @returns {{next: (function(): {value: *, done: boolean})}}
     */
    Help4.widget[Symbol.iterator] = () => {
        const {INSTANCES} = Help4.widget;
        let index = -1;

        return {
            next: () => ({
                value: INSTANCES[++index],
                done: !(index in INSTANCES)
            })
        };
    }

    /** sets focus to the Help4 framework */
    Help4.widget.focusHelp4 = () => {
        const activeWidget = Help4.widget.getActiveInstance();
        const panel = Help4.getController()?.getPanel();

        if (activeWidget) {
            // focus active widget
            activeWidget.focus();
        } else if (panel?.visible) {
            // focus panel if visible (home screen)
            panel.focus();
        } else {
            /**
             * ATTENTION: this case is currently not supported as {@link Help4.controller.CMP4#_enableHotkeys}
             * disables focus hotkeys on controller close
             */
            // focus instant help or callouts
            // const helpWidget = Help4.widget.getInstance('help');
            // helpWidget?.focus();
        }
    }

    /**
     * sets focus to the application; this works very unspecific and sets focus to
     * the first <a> or <>button> or <input> that is found on the page
     */
     Help4.widget.focusApp = async () => {
        const tourWidget = Help4.widget.getInstance('tour');
        if (tourWidget.isActive()) {
            const success = await tourWidget.focusApp();
            if (success) return;
        }

        const {
            Element: {isClassIncluded},
            CLASS_PREFIX
        } = Help4;

        ['a', 'button', 'input'].every(tagName => {
            const elements = document.getElementsByTagName(tagName);
            for (const element of elements) {
                if (!isClassIncluded(element, CLASS_PREFIX)) {  // ignore our own elements
                    element.focus();
                    return false;
                }
            }
            return true;
        });
    }


    /**
     * sets the focus to next item in the list - arrow keys
     * @param {string} key
     */
    Help4.widget.focusListItem = (key) => {
        const activeWidget = Help4.widget.getActiveInstance();
        if (activeWidget && Help4.includes(['up', 'down'], key)) {
            activeWidget.focusListItem(key);
            return;
        }

        Help4.getController().getPanel()?.focusListItem(key);
    }

    /**
     * gets the currently focussed element from the CMP4 shadow dom
     * @return {HTMLElement}
     */
    Help4.widget.getActiveElement = () => Help4.getController().getDom2('shadow').activeElement;

    /**
     * @param {Help4.widget.Widget} widget
     * @param {Help4.tracking.Tracking.TrackParams} params
     * @returns {Promise<void>}
     */
    Help4.widget.trackOpenClose = async (widget, params) => {
        const track = async (params) => {
            params.editMode = false;

            const {controller} = widget.getContext();
            const tracking = /** @type {Help4.tracking.Tracking} */ controller.getService('tracking');

            Help4.widget.TRACK_STATUS.params = {...params};
            params.type === 'whatsnew' && (params.type = 'help');

            await tracking.trackProject(params);
        }

        const {TRACK_STATUS: {params: TSP}} = Help4.widget;
        const name = widget.getName();

        const trackOpen = async (key) => {
            if (TSP.verb === 'open') {  // something is already tracked as open
                if (TSP.type === key) return;  // it's me; return
                await track({type: TSP.type, verb: 'close'});  // track close
            }
            await track(params);  // send track for open
        }

        const trackClose = async (key) => {
            if (TSP.verb !== 'open' || TSP.type !== key) return;  // I am not tracked as open; return
            await track(params);  // send track for close
        }

        switch (name) {
            case 'tour':
                // ignore tracking requests from Help4.widget.Widget for tours
                // tracking will be initialized from Help4.widget.tour.Widget
                if (!params.type) return;

                // fall-through is intentional
            case 'help':
            case 'whatsnew':
                params.type = name;
                params.verb === 'open'
                    ? await trackOpen(name)
                    : await trackClose(name);
                break;
        }
    }
})();