Source: theme/Theme.js

(function() {
    /**
     * @namespace theme
     * @memberof Help4
     */
    Help4.theme = {};

    /** Sets the CSS variables for a certain theme. */
    Help4.theme.Theme = class {
        static CSS_ID = 'help4-theme-core';
        static VAR_ID = 'help4-theme-vars';
        static BASE_ID = '_BASE';

        /**
         * @param {DocumentFragment} shadowRoot
         * @param {string} name - theme name
         * @param {Object} [dynamicValues = {}]
         */
        static async set(shadowRoot, name, dynamicValues = {}) {
            const themeCore = await _waitStylesheet(shadowRoot, this.CSS_ID);

            const apply = list => {
                const {VAR_ID} = this;
                let themeVars = shadowRoot.getElementById(VAR_ID);

                if (!themeVars) {
                    themeVars = document.createElement('style');
                    themeVars.id = VAR_ID;

                    const {nextElementSibling} = themeCore;
                    nextElementSibling
                        ? shadowRoot.insertBefore(themeVars, nextElementSibling)
                        : shadowRoot.appendChild(themeVars);
                }

                const html = [];
                for (const [name, rules] of Object.entries(list)) {
                     html.push(`:host, theme-${name} {\n${rules}\n}\n`);
                }
                themeVars.innerHTML = html.join('');
            };

            const {BASE_ID} = this;
            const {theme} = Help4;
            const added = [];
            const list = {};

            const add = name => {
                const vars = theme[name];
                if (vars) {
                    const {[BASE_ID]: base} = vars;
                    base && !Help4.includes(added, base) && add(base);

                    if (!Help4.includes(added, name)) {
                        const rules = Object.entries(vars)
                        .filter(([key]) => key !== BASE_ID)
                        .map(([key, value]) => {
                            if (dynamicValues[key]) value = dynamicValues[key];
                            return `${key}: ${value};`;
                        })
                        .join('\n');

                        added.push(name);
                        list[name] = rules;
                    }
                }
            };

            add('default');
            add(name);
            apply(list);
        }

        /**
         * @param {Object} values
         * @param {string} theme
         */
        static clamp(values, theme) {
            if (!values) return null;

            const colorClamp = [
                'panelHeadlineFg',
                'iconFg',
                'accentFg',
                'accentBg',
                'uiFg',
                'uiBg',
                'buttonFg',
                'buttonBg',
                'hoverFg',
                'hoverBg',
                'hoverListFg',
                'hoverListBg',
                'HSIconFg',
                'HSIconBg',
                'HSIconImpFg',
                'HSIconImpBg',
                'WNFg',
                'WNBg',
                'LABg',
                'LAAssetFg',
                'LAAssetBg',
                'LAHeadFg',
                'LAHeadBg',
                'LAHeadlineFg',
                'bubHeadFg',
                'bubHeadBg',
                'bubHeadBgCol'
            ];

            // clamp rgb values to rgb(...)
            for (const [key, value] of Object.entries(values)) {
                if (colorClamp.indexOf(key) >= 0 && value.includes(',') && !value.startsWith('rgb(')) {
                    values[key] = `rgb(${value})`;
                }
            }

            // remove whitespaces
            for (const [key, value] of Object.entries(values)) {
                if (typeof value === 'number') values[key] = value.toString();  // XRAY-5699
                values[key] = value.replace(/\s/mg, '');
            }

            const {bubHeadBgCol, HSIconFg, HSIconBg, HSIconImpFg, HSIconImpBg, buttonFg, buttonBg} = values;

            // legacy XRAY-2702 unify params
            if (bubHeadBgCol) {
                values.bubHeadBg = bubHeadBgCol;
                delete values.bubHeadBgCol;
            }

            // for legacy if HSIconFg or HSIconBg is set but no color information
            // about important hotspots, we use the same colors for important hotspots too
            if ((HSIconFg || HSIconBg) && (!HSIconImpFg && !HSIconImpBg)) {
                values.HSIconImpFg = HSIconFg;
                values.HSIconImpBg = HSIconBg;
            }

            if (!buttonFg && !buttonBg) {
                values.buttonFg = 'var(--help4-cmp-act-fg-col)';
                values.buttonBg = 'var(--help4-cmp-act-bg-col)';
            }

            // get custom theme base
            const {themeBase = theme || 'horizon'} = values;
            delete values.themeBase;  // avoid wrong processing in next loop

            // add prefix to keys for correct CSS var names
            const prefix = '--help4-cmp-';
            return Object.entries(values)
            .reduce((a, [key, value]) => {
                a[`${prefix}${key}`] = value;
                return a;
            }, {themeBase});  // re-add theme base here
        }

        /**
         * @param {DocumentFragment} shadowRoot
         * @param {string} name
         * @param {Object} list
         * @throws {Error}
         */
        static setCustomVariables(shadowRoot, name, list) {
            const {VAR_ID} = this;
            const id = `${VAR_ID}-${name}`;

            const rules = Object.entries(list)
            .map(([name, value]) => `${name}: ${value};`)
            .join('\n');

            let style = shadowRoot.getElementById(id)
            if (!style) {
                style = document.createElement('style');
                style.id = id;
                shadowRoot.appendChild(style);
            }

            style.innerHTML = `:host, ${name} {\n${rules}\n}`;
        }

        /**
         * @param {DocumentFragment} shadowRoot
         * @param {string} name
         */
        static removeCustomVariables(shadowRoot, name) {
            const {VAR_ID} = this;
            const style = shadowRoot.getElementById(`${VAR_ID}-${name}`);
            style && style.parentNode.removeChild(style);
        }
    }

    /**
     * @memberof Help4.theme.Theme
     * @private
     * @param {DocumentFragment} shadowRoot
     * @param {string} cssId
     * @returns {Promise<HTMLElement>}
     */
    async function _waitStylesheet(shadowRoot, cssId) {
        return new Help4.Promise(resolve => {
            const wait = () => {
                const cssFile = shadowRoot.getElementById(cssId);
                cssFile
                    ? resolve(cssFile)
                    : setTimeout(wait, 100);
            }
            wait();
        });
    }
})();