/**
 * Visuals - Industrial Visualization Framework for JavaScript
 *
 * Copyright © 2012-2025 Smart HMI GmbH
 *
 * All rights reserved
 *
 * No part of this website or any of its contents may be reproduced, copied, modified or
 * adapted, without the prior written permission of Smart HMI.
 *
 * Commercial use and distribution of the contents of the website is not allowed without
 * express and prior written permission of Smart HMI.
 *
 *
 * Web: http://www.smart-hmi.de
 *
 * @version 2.16.3 006ab051.63351 07-04-2025 07:49:54
 */

/**
 * Alpha Num Keyboard
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * label {string}: label for the overlay container
 * key-preview {boolean}, shows button name preview on touch pressed only
 * auto-key-focus {boolean}: false, // sets tabindex="0" on each key
 * show-enter {boolean]: false, // show the enter button, also disable enter to apply input
 * password-input {boolean}: false, display input as password --> input via real keyboard disabled
 * select-box-enabled {boolean}: true, show keyboard select box
 * value {}: null, // existing value of e.g. input-field
 * callback {function}: null, used to pass value to other origin-control
 *
 * Additional information about the configuration is located in the tutorial: keyboard-layout-configuration
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "AlphaNumKeyBoard", // control name in camel-case
        uiType = "alpha-num-keyboard", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/alpha-num-keyboard",
        "label": "",
        "key-preview": true, // shown on touch only
        "show-enter": false,
        "auto-key-focus": false, // set tabindex="0" to key
        "password-input": false,
        "select-box-enabled": true,
        "keyboards": {}
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            listeners: [],
            controls: [],
            tokens: [],
            keys: [],
            keyboardMap: {},
            activeModifiers: {},
            activeKeyboard: null,
            activePalette: null,
            activeKeys: null,
            currentText: "",
            keyTypeListeners: {},
            previewContainer: null,
            shiftKeyActive: false,
            showPassword: false
        },
        /* imports added at runtime */
        imports: {
        },
        events: [],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const { InputHandler: KeyboardInputHandler } = shmi.requires("visuals.tools.input.virtualKeyboard"),
                    self = this;

                // listeners add by shmi mouse and touch listeners
                self.vars.keyListeners = {
                    onPress: onKeyPressListener.bind(null, self),
                    onClick: onKeyClickListener.bind(null, self),
                    onRelease: onKeyReleaseListener.bind(null, self)
                };

                // custom listeners added with browser method 'addEventListener'
                self.vars.eventListeners = {
                    mouseOver: onKeyMouseInListener.bind(null, self),
                    mouseOut: onKeyMouseOutListener.bind(null, self),
                    mouseUp: onKeyMouseUpListener.bind(null, self),
                    mouseDown: onKeyMouseDownListener.bind(null, self),
                    touchStart: onKeyTouchStartListener.bind(null, self),
                    touchEnd: onKeyTouchEndListener.bind(null, self),
                    touchMove: onKeyTouchMoveListener.bind(null, self),
                    keyDown: onKeyDownListener.bind(null, self),
                    keyUp: onKeyUpListener.bind(null, self)
                };

                // html element to insert keyboard
                self.vars.keyboardContainer = shmi.getUiElement('keyboard-container', self.element);

                // get configurations
                var keyboardConfig = shmi.requires("shmi.visuals.session.keyboards"),
                    localeInfo = shmi.requires("visuals.session.localeInfo"),
                    um = shmi.requires("visuals.session.UserManager");

                if (!keyboardConfig) {
                    return;
                }
                self.config.keyboards = keyboardConfig;

                // get and set current locale
                if (um.currentUser && um.currentUser.loggedIn && keyboardConfig && localeInfo.locales[um.currentUser.locale] &&
                    localeInfo.locales[um.currentUser.locale].keyboard && keyboardConfig[localeInfo.locales[um.currentUser.locale].keyboard]) {
                    self.vars.currentLocale = localeInfo.locales[um.currentUser.locale].keyboard;
                } else if (localeInfo.locales[localeInfo.default] && localeInfo.locales[localeInfo.default].keyboard &&
                    keyboardConfig[localeInfo.locales[localeInfo.default].keyboard]) {
                    self.vars.currentLocale = localeInfo.locales[localeInfo.default].keyboard;
                } // if currentLocale coun't be set the first keyboard configuration is used

                // init keyboard
                var languages = Object.keys(keyboardConfig); // keys/names of all configured keyboard languages

                // initialize seperate keyboard control like keyboard language selection
                initKeyboardControls(self);

                languages.forEach(function(langName) {
                    // create keyboards for each language registered
                    var keyboard = createKeyboard(self, langName, keyboardConfig[langName]);
                    self.vars.keyboardContainer.appendChild(keyboard.element);
                    // save the keyboard in a map with name as key (keyboard is not a control!)
                    self.vars.keyboardMap[langName] = keyboard;
                });

                // has been temporary preview container, can be used as final preview container if needed
                self.vars.previewContainer = shmi.getUiElement('text-preview', self.element);
                self.vars.inputHandler = new KeyboardInputHandler(self.vars.previewContainer, {
                    value: self.config.value ?? "",
                    passwordCharacter: "●",
                    passwordMode: !!self.config['password-input'],
                    callback: (text) => self.vars.currentText = text
                });
                self.vars.previewContainer.focus();

                var activePaletteElement = null;
                // set initially active keyboard and palette name
                if (self.vars.currentLocale) {
                    self.vars.activeKeyboard = self.vars.keyboardMap[self.vars.currentLocale];
                } else {
                    self.vars.activeKeyboard = self.vars.keyboardMap[Object.keys(self.vars.keyboardMap)[0]];
                }
                activePaletteElement = Object.keys(self.vars.activeKeyboard.paletteMap)[0];

                // set initial activePalette and activeKeyMap to be referenced later
                self.vars.activePalette = self.vars.activeKeyboard.paletteMap[activePaletteElement];
                self.vars.activeKeyMap = self.vars.activePalette.keyMap;

                // initially show the first keyboard while all others are hidden by default with display: none!
                self.vars.activeKeyboard.element.style.display = 'block';

                if (self.config['key-preview']) {
                    // add css class "key-preview-enabled" to enable the popup of the key name preview
                    shmi.addClass(self.element, 'key-preview-enabled');
                }

                if (self.config['password-input']) {
                    shmi.addClass(self.element, 'password-input');
                }

                if (!self.config['show-enter']) {
                    const textArea = shmi.getUiElement('text-preview', self.element);
                    textArea.rows = '1';

                    shmi.addClass(self.element, 'hide-enter');
                }

                // set/enable listeners for each key in active key map
                enableActiveKeyMapListeners(self);

                const keyboardBackground = shmi.getUiElement('keyboard-background', self.element),
                    keyboardBox = shmi.getUiElement('keyboard-content', self.element),
                    keyboardTitle = shmi.getUiElement('keyboard-title', self.element);

                let initialX,
                    initialY,
                    initialKeyboardPosition,
                    backgroundDim;

                const dragEventFunctions = {
                    onDrag: function(dx, dy, e) {
                        let currentX = initialKeyboardPosition.x + e.clientX - initialX,
                            currentY = initialKeyboardPosition.y + e.clientY - initialY;

                        currentX = Math.max(0, Math.min(backgroundDim.width - initialKeyboardPosition.width, currentX));
                        currentY = Math.max(0, Math.min(backgroundDim.height - initialKeyboardPosition.height, currentY));
                        keyboardBox.style.left = `${currentX}px`;
                        keyboardBox.style.top = `${currentY}px`;
                    },
                    onPress: function(posX, posY) {
                        initialX = posX;
                        initialY = posY;
                        initialKeyboardPosition = keyboardBox.getBoundingClientRect();
                        backgroundDim = keyboardBackground.getBoundingClientRect();
                    }
                };

                self.vars.listeners.push(new shmi.visuals.io.MouseListener(keyboardTitle, dragEventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(keyboardTitle, dragEventFunctions));
            },
            /**
             * Enables the Alpha Num Keyboard
             */
            onEnable: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.vars.controls.forEach(function(c) {
                    c.enable();
                });
                self.vars.inputHandler.enable();

                document.addEventListener('keydown', self.vars.eventListeners.keyDown);
                document.addEventListener('keyup', self.vars.eventListeners.keyUp);
                shmi.log("[Alpha Num Keyboard] enabled", 1);
            },
            /**
             * Disables the Alpha Num Keyboard
             *
             */
            onDisable: function() {
                var self = this;
                self.hide();

                document.removeEventListener('keydown', self.vars.eventListeners.keyDown);
                document.removeEventListener('keyup', self.vars.eventListeners.keyUp);

                self.vars.inputHandler.disable();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.vars.controls.forEach(function(c) {
                    c.disable();
                });
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                shmi.log("[Alpha Num Keyboard] disabled", 1);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.vars.controls.forEach(function(c) {
                    c.lock();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.vars.controls.forEach(function(c) {
                    c.unlock();
                });
            },
            /**
             * Displays the Alpha Num Keyboard
             */
            show: function() {
                shmi.removeClass(this.element, 'closed');
                shmi.addClass(this.element, 'open');
            },
            /**
             * Hides the Alpha Num Keyboard from DOM
             */
            hide: function() {
                shmi.removeClass(this.element, 'open');
                shmi.addClass(this.element, 'closed');
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(val, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    // #        Building the Keyboard       # //

    /**
     * initiate creation of keyboard
     *
     * @param {object} self
     * @param {string} keyboardName name of keyboard - de,us,...
     * @param {object} keyboardConfig configuration of keyboard
     * @returns {{paletteNames: string[], name: *, element: HTMLElement}} Object that contains the palette names, the keyboard name and element
     */
    function createKeyboard(self, keyboardName, keyboardConfig) {
        var paletteNames = Object.keys(keyboardConfig),
            keyboardElement = document.createElement('DIV'),
            keyboardPaletteMap = {};

        shmi.addClass(keyboardElement, 'iqel-keyboard');
        shmi.addClass(keyboardElement, 'keyboard-' + keyboardName.toLowerCase());
        keyboardElement.setAttribute('data-name', 'keyboard-' + keyboardName);

        paletteNames.forEach(function(paletteName, idx) {
            var paletteConfig = keyboardConfig[paletteName],
                palette = createKeyboardPalette(self, paletteName, paletteConfig),
                id = getUnusedId(keyboardPaletteMap);
            palette.id = id;
            palette.element.setAttribute('data-key-id', id);

            // sets display on first palette of the keyboard to flex and others to none to show the first one initially
            palette.element.style.display = (idx === 0 ? 'flex' : 'none');
            keyboardElement.appendChild(palette.element);
            keyboardPaletteMap[id] = palette;
        });

        return {
            paletteMap: keyboardPaletteMap,
            name: keyboardName,
            element: keyboardElement
        };
    }

    /**
     * creates a keyboard palette, wich represents one face
     * (e.g. letter, numbers, special characters, ...)
     *
     * @param {object} self
     * @param {string} paletteName name of palette
     * @param {object} paletteRows configuration of key-rows from palette
     * @returns {object} palette with name, keymap and element
     */
    function createKeyboardPalette(self, paletteName, paletteRows) {
        var paletteElement = document.createElement('DIV'),
            createdPaletteRows = [],
            keyboardPaletteKeyMap = {};

        shmi.addClass(paletteElement, 'iqel-keyboard-palette');
        shmi.addClass(paletteElement, 'keyboard-palette-' + paletteName.toLowerCase());
        paletteElement.setAttribute('data-name', 'palette-' + paletteName);

        // creating rows of keys for the palette
        paletteRows.forEach(function(rowConfig) {
            var paletteRow = createKeyboardRow(self, rowConfig);
            paletteElement.appendChild(paletteRow.element);
            createdPaletteRows.push(paletteRow);
            // creating keys for each row
            paletteRow.keys.forEach(function(key) {
                var id = getUnusedId(keyboardPaletteKeyMap);
                key["data-key-id"] = id;
                key.element.setAttribute('data-key-id', id);
                keyboardPaletteKeyMap[key["data-key-id"]] = key;
            });
        });

        return {
            name: paletteName,
            element: paletteElement,
            keyMap: keyboardPaletteKeyMap
        };
    }

    /**
     * create a keyboard row, which contains an variable amount of keys
     *
     * @param {object} self
     * @param {object} rowConfig configuration of the keys contained in this row
     * @returns {object} row with element and created keys
     */
    function createKeyboardRow(self, rowConfig) {
        var keyRow = document.createElement('DIV'),
            rowKeys = [];
        shmi.addClass(keyRow, 'iqel-key-row');
        rowConfig.forEach(function(keyConfig, idx) {
            var key = createKeyboardKey(self, keyConfig.name, keyConfig, (idx < rowConfig.length / 2));
            keyRow.appendChild(key.element);
            rowKeys.push(key);
            if (key.alternatives) {
                key.alternatives.forEach(function(alt) {
                    rowKeys.push(alt);
                });
            }
        });

        return {
            keys: rowKeys,
            element: keyRow
        };
    }

    /**
     * create a keyboard key
     *
     * @param {object} self
     * @param {string} keyName  id to identify key later
     * @param {object} keyConfig configuration of key
     * @returns {object} key with name, id, configuration and element
     */
    function createKeyboardKey(self, keyName, keyConfig, leftSide) {
        const keyBox = document.createElement('DIV'),
            key = document.createElement('DIV'),
            { applyIcon } = shmi.requires("visuals.tools.icons");

        let keyPrev = null,
            keyPrevContent = null,
            alternativeKeys = null;

        if (self.config['key-preview']) {
            keyPrev = document.createElement('DIV');
            keyPrevContent = document.createElement('SPAN');
        }

        // register all used key types to the listeners array
        self.vars.keyTypeListeners[keyConfig.type] = self.vars.keyTypeListeners[keyConfig.type] || {};

        const escapedKeyName = String(keyConfig.name || keyName).replace(/"/g, '\\"');
        shmi.addClass(keyBox, 'iqel-key-box');
        shmi.addClass(key, 'iqel-key');
        shmi.addClass(key, 'key-' + escapedKeyName);
        key.setAttribute('data-key-name', escapedKeyName);

        if (self.config["auto-key-focus"]) {
            key.setAttribute('tabindex', 0);
        }

        if (keyConfig['class-name']) {
            shmi.addClass(key, keyConfig['class-name']);
        }

        if (keyConfig.size !== undefined) {
            shmi.addClass(keyBox, 'key-box-flex-' + keyConfig.size);
        }

        shmi.addClass(key, 'key-' + keyConfig.type);
        key.setAttribute('data-key-type', keyConfig.type);

        if (keyConfig.iconURL) {
            applyIcon(key, keyConfig.iconURL, null, true);
        } else {
            key.innerText = keyConfig.name || '';
        }

        keyBox.appendChild(key);

        // create preview container
        if (keyPrev) {
            shmi.addClass(keyPrev, 'iqel-key-preview');
            keyPrevContent.innerText = keyConfig.name || '';
            keyBox.appendChild(keyPrev);
            keyPrev.appendChild(keyPrevContent);
        }

        // create alternative-key container and keys
        if (keyConfig.alternatives) {
            alternativeKeys = document.createElement('DIV');
            shmi.addClass(alternativeKeys, 'alt-keys');
            if (leftSide) {
                shmi.addClass(alternativeKeys, 'left-side');
            } else {
                shmi.addClass(alternativeKeys, 'right-side');
            }
            keyConfig.alternatives.forEach(function(config) {
                const altKey = createKeyboardKey(self, config.name, config);
                alternativeKeys.appendChild(altKey.element);
            });
            shmi.addClass(alternativeKeys, 'numofKeys-' + keyConfig.alternatives.length);
            keyBox.appendChild(alternativeKeys);
        }
        keyConfig.element = keyBox;
        return keyConfig;
    }

    /**
     * update key config when modifier (shift) is pressed
     *
     * @param {object} self
     * @param {object} keyConfig configuration of key
     * @param {object} values modifier values
     */
    function setKeyConfig(self, keyConfig, values) {
        // set html key value
        const newName = values.name || keyConfig.name;
        if (newName) {
            if (keyConfig.hasIcon !== true) {
                keyConfig.element.firstChild.textContent = newName;
            }

            if (self.config['key-preview']) {
                const prevElem = keyConfig.element.getElementsByTagName('span')[0];
                if (prevElem) {
                    prevElem.innerText = newName;
                }
            }
        }

        // set class for key element
        const newClassName = values['class-name'];
        if (newClassName) {
            if (newClassName.charAt(0) === '!') {
                shmi.removeClass(keyConfig.element, newClassName.substring(1));
            } else {
                shmi.addClass(keyConfig.element, newClassName);
            }
        }

        // set html key size by class name
        const css = keyConfig.element.className,
            size = values.size || keyConfig.size,
            sizeClassName = size ? 'key-box-flex-' + size : null;
        if (keyConfig.element.className.indexOf(sizeClassName) === -1 && sizeClassName) {
            keyConfig.element.className = (" " + css + " ").replace(/\bkey-box-flex-\S*\b/g, ' ');
            shmi.addClass(keyConfig.element, sizeClassName);
        }

        // set html key type
        const type = values.type || keyConfig.type;
        keyConfig.element.firstChild.setAttribute('data-key-type', type);
    }

    /**
     * enable listener of currently displayed keys
     *
     * @param {object} self
     */
    function enableActiveKeyMapListeners(self) {
        Object.keys(self.vars.activeKeyMap).forEach(function(key) {
            var ml = new shmi.visuals.io.MouseListener(self.vars.activeKeyMap[key].element, self.vars.keyListeners),
                tl = new shmi.visuals.io.TouchListener(self.vars.activeKeyMap[key].element, self.vars.keyListeners),
                keyElem = self.vars.activeKeyMap[key].element.firstChild;
            ml.enable();
            tl.enable();
            self.vars.listeners.push(ml, tl);

            // mouse
            keyElem.addEventListener('mouseover', self.vars.eventListeners.mouseOver);
            keyElem.addEventListener('mouseout', self.vars.eventListeners.mouseOut);
            keyElem.addEventListener('mousedown', self.vars.eventListeners.mouseDown);
            keyElem.addEventListener('mouseup', self.vars.eventListeners.mouseUp);
        });

        // touch
        self.vars.activePalette.element.addEventListener('touchstart', self.vars.eventListeners.touchStart);
    }

    /**
     * disable listener of currently displayed keys
     *
     * @param {object} self
     */
    function disableActiveKeyMapListeners(self) {
        self.vars.listeners.forEach(function(listener) {
            listener.disable();
        });
        self.vars.listeners = [];

        Object.keys(self.vars.activeKeyMap).forEach(function(key) {
            var keyElem = self.vars.activeKeyMap[key].element.firstChild;
            keyElem.removeEventListener('mouseover', self.vars.eventListeners.mouseOver);
            keyElem.removeEventListener('mousedown', self.vars.eventListeners.mouseDown);
            keyElem.removeEventListener('mouseout', self.vars.eventListeners.mouseOut);
            keyElem.removeEventListener('mouseup', self.vars.eventListeners.mouseUp);

            self.vars.activePalette.element.removeEventListener('touchstart', self.vars.eventListeners.touchStart);
        });
    }

    /**
     * init Keyboard Controls
     *  - selectBox for keyboard selection
     *  - submit button
     *  - close/abort button
     *
     * @param {object} self
     */
    function initKeyboardControls(self) {
        if (self.config["select-box-enabled"] && Object.keys(self.config.keyboards).length > 1) {
            // selectbox if multiple keyboards are available
            // language selection
            const localeInfo = shmi.requires("visuals.session.localeInfo"),
                languageCtrl = shmi.getUiElement('language-control', self.element),
                selectBoxConfig = {
                    "label": "",
                    "options": []
                },
                languages = Object.keys(self.config.keyboards);

            if (languages && languages.length > 0) {
                languages.forEach(function(langName, idx) {
                    selectBoxConfig.options.push({
                        "label": localeInfo.keyboards[langName].label,
                        "value": langName
                    });
                });
                const selectBox = shmi.createControl("iq-select-box", languageCtrl, selectBoxConfig, "DIV");
                shmi.waitOnInit(selectBox, function() {
                    // default selection
                    if (self.vars.currentLocale) {
                        selectBox.setValue(self.vars.currentLocale);
                    } else {
                        self.config['default-keyboard'] = self.config['default-keyboard'] || languages[0];
                        selectBox.setValue(self.config['default-keyboard']);
                    }

                    self.vars.tokens.push(selectBox.listen("change", function onChange(evt) {
                        // disable and hide current keyboard
                        disableActiveKeyMapListeners(self);
                        self.vars.activePalette.element.style.display = 'none';
                        self.vars.activeKeyboard.element.style.display = 'none';

                        // reset active keyboard and palette
                        self.vars.activeKeyboard = self.vars.keyboardMap[evt.detail.value];
                        const activePaletteElement = Object.keys(self.vars.activeKeyboard.paletteMap)[0];
                        self.vars.activePalette = self.vars.activeKeyboard.paletteMap[activePaletteElement];
                        self.vars.activeKeyMap = self.vars.activePalette.keyMap;

                        // make keyboard visible
                        self.vars.activeKeyboard.element.style.display = 'block';
                        self.vars.activePalette.element.style.display = 'flex';

                        enableActiveKeyMapListeners(self);
                    }));
                });
                shmi.addClass(languageCtrl, 'enabled');
                self.vars.controls.push(selectBox);
            }
        }

        // close button
        const closeButtonContainer = shmi.getUiElement('close-button', self.element),
            closeButtonConfig = {
                "label": null,
                "icon-src": "pics/system/controls/dialog-box/close.svg",
                "show-icon": true,
                "show-text": false,
                "inline-svg-icon": true
            },
            closeButton = shmi.createControl("iq-button", closeButtonContainer, closeButtonConfig, "DIV");

        self.vars.tokens.push(
            closeButton.listen("click", function onclick(evt) {
                log("Fire: Close Keyboard");
                shmi.fire('keyboard-cancel', {
                    "success": false,
                    "input": self.vars.currentText
                });
            })
        );
        self.vars.controls.push(closeButton);

        // submit button
        const submitButtonContainer = shmi.getUiElement('submit-button', self.element),
            submitButtonConfig = {
                "class-name": "iq-button iq-variant-01 filled",
                "icon-src": "pics/system/controls/alpha-num-keyboard/ok_done.svg",
                "show-icon": true,
                "show-text": false,
                "label": null,
                "inline-svg-icon": true
            },
            submitButton = shmi.createControl("iq-button", submitButtonContainer, submitButtonConfig, "DIV");

        self.vars.tokens.push(
            submitButton.listen("click", function onclick(evt) {
                log("Fire: Submit Keyboard");
                shmi.fire('keyboard-submit', {
                    "success": true,
                    "input": self.vars.currentText
                });
            })
        );
        self.vars.controls.push(submitButton);

        // pw button
        if (self.config["password-input"]) {
            const pwButtonContainer = shmi.getUiElement('pw-button', self.element),
                pwButtonConfig = {
                    "icon-src": "pics/system/controls/alpha-num-keyboard/eye.svg",
                    "show-icon": true,
                    "show-text": false,
                    "label": null,
                    "inline-svg-icon": true
                },
                pwButton = shmi.createControl("iq-button", pwButtonContainer, pwButtonConfig, "DIV");

            self.vars.tokens.push(
                pwButton.listen("click", function onclick(evt) {
                    self.vars.showPassword = !self.vars.showPassword;
                    self.vars.inputHandler.setPasswordMode(!self.vars.showPassword);
                })
            );
            self.vars.controls.push(pwButton);
        }
    }

    var idAlphabet = [
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
    ];

    /**
     * getRandomId - generates random ID string of variable length
     *
     * @param  {number} length length of generated ID string
     * @return {string}        random ID
     */
    function getRandomId(length) {
        var rid = "";
        if (length === undefined) {
            length = 3;
        }
        for (var i = 0; i < length; i++) {
            rid += idAlphabet[Math.floor(Math.random() * (idAlphabet.length - 1))];
        }
        return rid;
    }

    /**
     * Get a unused random id
     *
     * @param obj unused in this context
     * @return    unused id
     */
    function getUnusedId(obj) {
        var id = null;
        do {
            id = getRandomId(8);
        } while (shmi.objectHasOwnProperty(obj, id));
        return id;
    }

    // #        Keyboard handler        # //

    /**
     * fireKeyboardClick - fires action of a pressed/clicked keyboard key
     *
     * @param {object} self
     * @param {object} key key where the event was triggert
     * @param {object} event event from mouse, key or touch event
     */
    function fireKeyboardClick(self, key, event) {
        // execute action depending on the type of the pressed/clicked key
        switch (key.type) {
        case 'letter':
        case 'number':
        case 'character':
            // simply add the value of the pressed key to the preview text content
            insertLetterIntoCurrentText(self, key);
            break;
        case 'palette':
            // a key of type palette has to have a palette attribute with the name of the palette to be activated
            if (key.palette) {
                setKeyboardPalette(self, key);
            } else {
                console.error('[alpha-num-keyboard] Missing palette attribute value on key of type palette');
            }
            break;
        case 'modifier':
            // a key of type modifier has to have a property value of modify
            var modConfig = key.modify ? key.modify.split(':') : null,
                type, mode, name;
            // modify value has to contain type:mode:name
            if (modConfig && modConfig.length === 3) {
                type = modConfig[0];
                mode = modConfig[1];
                name = modConfig[2];
                if (!isModifierActive(self, name)) {
                    setModifier(self, name, mode, type, event);
                } else {
                    // Check if this is unset twice in case of the clip or clipOnce callback fires too!
                    // The unsetModifier method only executes if the modifier is still active
                    shmi.removeClass(event.target, 'clipped');
                    unsetModifier(self, name, mode, type, event);
                }
            }
            break;
        case 'resolver':
            keyResolver(self, key);
            break;
        default:
            log('uncatched keyboard event: ' + key.type);
        }
    }

    /**
     * is fired when a key with type: resolver is pressed
     * (e.g. backspace, submit, ..)
     *
     * @param {object} self
     * @param {object} key key where the event was triggert
     */
    function keyResolver(self, key) {
        var resolve = key.resolve;
        if (resolve) {
            // define addition actions on resolve which is meant to modify the current text
            // add any resolve case with further methods
            switch (resolve) {
            case 'backspace':
                self.vars.inputHandler.deleteBackward();
                break;
            case 'submit':
                log("Fire: Submit Keyboard");
                shmi.fire('keyboard-submit', {
                    "success": true,
                    "input": self.vars.currentText
                });
                break;
            default:
                log("Missing Default Case");
            }
        }
    }

    /**
     * set Modifier
     * a modifier is somthing like `shift` or `alt`
     *
     * @param {object} self
     * @param {string} name  name of modifiers, like `shift`
     * @param {string} mode modifier-mode for the switch-case, like `clip` or `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {object} setEvent event from mouse, key or touch event
     */
    function setModifier(self, name, mode, type, setEvent) {
        if (!isModifierActive(self, name)) {
            var id = getActivePaletteId(self);
            self.vars.activeModifiers[id] = self.vars.activeModifiers[id] || [];
            self.vars.activeModifiers[id].push(name);
            // the unsetModifier is registered with the modification mode and type listener
            createModificationTypeListeners(self, name, mode, type, setEvent);
            Object.keys(self.vars.activeKeyMap).forEach(function(key) {
                if (self.vars.activeKeyMap[key].type === type) {
                    var values = evaluateKeyModifiers(self, self.vars.activeKeyMap[key]);
                    setKeyConfig(self, self.vars.activeKeyMap[key], values);
                }
            });
        }
    }

    /**
     * unset Modifiers
     * a modifier is somthing like `shift` or `alt`
     *
     * @param {object} self
     * @param {string} name  name of modifiers, like `shift`
     * @param {string} mode modifier-mode for the switch-case, like `clip` or `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {object} event event from mouse, key or touch event
     */
    function unsetModifier(self, name, mode, type, event) {
        if (isModifierActive(self, name)) {
            var id = getActivePaletteId(self),
                idx = self.vars.activeModifiers[id].indexOf(name);
            // remove the of the active modifiers array
            self.vars.activeModifiers[id].splice(idx, 1);
            Object.keys(self.vars.activeKeyMap).forEach(function(key) {
                var values = evaluateKeyModifiers(self, self.vars.activeKeyMap[key]);
                setKeyConfig(self, self.vars.activeKeyMap[key], values);
            });
        }
    }

    /**
     * create function for when modifier-mode is `clip`
     * (deutsch: dauerhaftes Feststell dieser Taste)
     *
     * @param {object} self
     * @param {string} name name of modifiers, like `shift`
     * @param {string} mode modifier-mode, `clip`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {string} id   identifier
     * @param {object} event event from mouse, key or touch event
     */
    function createClipModifierFunction(self, name, mode, type, id, event) {
        return function modClip(unsetEvent, key) {
            // listener callback gets triggered on each click on a button of the specified type
            if (unsetEvent !== event && key.modify === type + ':' + mode + ':' + name) {
                // only unset the clip modifier if key has the same modify type
                delete self.vars.keyTypeListeners[type][mode][id]; // remove registered listener callback
                unsetModifier(self, name, mode, type, event); // unset the modifer
                return false; // prevent any further actions: boolean
            }
            return true;
        };
    }

    /**
     * create function for when modifier-mode is `clipOnce`
     * (deutsch: einmaliges Feststell dieser Taste)
     *
     * @param {object} self
     * @param {string} name name of modifiers, like `shift`
     * @param {string} mode modifier-mode, `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {string} id   identifier
     * @param {object} event event from mouse, key or touch event
     */
    function createClipOnceModifierFunction(self, name, mode, type, id, event) {
        return function modClipOnce(unsetEvent, key) {
            // listener callback gets triggered on each click on a button of the specified type
            if (unsetEvent !== event) {
                // only unset the modifier if it did not get set by the current event
                if (key.type === type || key.modify === type + ':' + mode + ':' + name) {
                    // only unset modifier if the pressed key is of type being modified or has same modify type
                    shmi.removeClass(event.target, 'clipped');
                    delete self.vars.keyTypeListeners[type][mode][id];
                    unsetModifier(self, name, mode, type, event);
                    return false; // prevent any further actions: boolean
                }
            }
            return true;
        };
    }

    /**
     * creates listener and function that is called, when button is pressed and a modifier is active
     *
     * @param {object} self
     * @param {string} name name of modifiers, like `shift`
     * @param {string} mode modifier-mode for the switch-case, like `clip` or `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {object} event event from mouse, key or touch event
     */
    function createModificationTypeListeners(self, name, mode, type, event) {
        var id = getRandomId(8);
        if (self && name && mode && type && event) {
            // these are different types of modifier modes
            switch (mode) {
            // the clip modifier mode keeps the modifier enabled until the button gets pressed again
            case 'clip':
                // ensure the mode is an object
                self.vars.keyTypeListeners[type][mode] = self.vars.keyTypeListeners[type][mode] || {};
                // define callback in listener for a pressed button of specific type and mode identified by a random id
                self.vars.keyTypeListeners[type][mode][id] = createClipModifierFunction(self, name, mode, type, id, event);
                break;
            // the clipOnce modifier mode keeps the modifier enabled until one other button is pressed
            case 'clipOnce':
                // add class 'clipped', contains the same style as 'pressed', doesn't get removed on release
                shmi.addClass(event.target, 'clipped');
                // ensure key type listener for mode is an object
                self.vars.keyTypeListeners[type][mode] = self.vars.keyTypeListeners[type][mode] || {};
                // define callback in listener for a pressed button of specific type and mode identified by a random id
                self.vars.keyTypeListeners[type][mode][id] = createClipOnceModifierFunction(self, name, mode, type, id, event);
                break;
            default:
                log("Undefined modifier mode", mode);
            }
        }
    }

    /**
     * get all modifier of given key
     *
     * @param {object} self
     * @param {object} key key context
     * @returns {object} modifier of key
     */
    function evaluateKeyModifiers(self, key) {
        const values = {},
            modifiers = shmi.cloneObject(key.modifiers),
            iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

        iterObj(modifiers || {}, (mapped, modifier) => {
            const modVars = modifier.split(':');
            if (modVars.length !== 2) {
                console.error("[Keyboard] - modifier not configured right", key);
            } else if (isModifierActive(self, modVars[0])) {
                // add class if modifier is active
                values[modVars[1]] = mapped;
            } else if (modVars[1] === 'class-name') {
                // and remove it if the modifier is inactive
                values[modVars[1]] = '!' + mapped;
            }
        });

        return values;
    }

    /**
     * set keyboard palette by given key config
     *
     * @param {object} self
     * @param {object} key key, that initiates palatte change
     */
    function setKeyboardPalette(self, key) {
        var paletteName = key.palette,
            newPalette = null;

        // search for palette with paletteName within available palettes of active keyboard
        newPalette = Object.values(self.vars.activeKeyboard.paletteMap).filter(function(palette) {
            return palette && (palette.name === (paletteName));
        })[0];

        if (newPalette) {
            // hide current palette
            self.vars.activePalette.element.style.display = 'none';

            // disable all listeners of the current active palette
            disableActiveKeyMapListeners(self);

            self.vars.activePalette = newPalette;
            self.vars.activeKeyMap = self.vars.activePalette.keyMap;

            // show new palette
            // important: shown palette element has to be display flex
            self.vars.activePalette.element.style.display = 'flex';

            // enable the new active palette key listeners
            enableActiveKeyMapListeners(self);
        } else {
            console.error('[Keyboard] - can not find palette with name: "' + paletteName + '"');
        }
    }

    /**
     * check if given modifier is active
     *
     * @param {object} self
     * @param {string} modifierName name of modifier
     * @returns {boolean} if modifier by modifier name is currently active or not
     */
    function isModifierActive(self, modifierName) {
        var id = getActivePaletteId(self);
        return self.vars.activeModifiers[id] && self.vars.activeModifiers[id].indexOf(modifierName) !== -1;
    }

    /**
     * get id of active palette
     *
     * @returns {string}
     */
    function getActivePaletteId(self) {
        return self.vars.activeKeyboard.name.toLowerCase() + ':' + self.vars.activePalette.name.toLowerCase();
    }

    /**
     * removeAllPressedHighlights - removes all `pressed` classes.
     * (this is why we have a clipped class with the same styles too ;) )
     *
     * @param {object} self
     */
    function removeAllPressedHighlights(self) {
        self.vars.activePalette.element.querySelectorAll('.iqel-key.pressed').forEach(function(pressedElement) {
            shmi.removeClass(pressedElement, 'pressed');
        });
    }

    /**
     * removeAllAltKeys - removes all `show-alt-keys` classes.
     *
     * @param {object} self
     */
    function removeAllAltKeys(self) {
        clearTimeout(self.vars.altKeysTimeout); // removes timeout to show alternative keys
        self.vars.activePalette.element.querySelectorAll('.iqel-key.show-alt-keys').forEach(function(pressedElement) {
            shmi.removeClass(pressedElement, 'show-alt-keys');
        });
    }

    /**
     * fire type listener
     *
     * @param {object} self
     * @param {object} key key configuration
     * @param {object} event event from mouse, key or touch event
     * @returns {boolean} if further actions should be prevented or be allowed
     */
    function fireTypeListeners(self, key, event) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject;
        var type = key.type,
            prevented = false;
        if (type) {
            var typeListeners = self.vars.keyTypeListeners[type];
            iter(typeListeners, function(listenerCallbacks, mode) {
                iter(listenerCallbacks, function(callback, id) {
                    if (callback(event, key) === false) {
                        prevented = true;
                    }
                });
            });
        }
        return !prevented;
    }

    /**
     * get key by value
     *
     * @param {object} self
     * @param {object} event event from mouse, key or touch event
     * @returns {object} key with the given value
     */
    function getKeyByValue(self, event) {
        var keyMap = null;
        keyMap = Object.values(self.vars.activeKeyMap).filter(function(key) {
            var eventKey = event.key.toLowerCase(),
                keyName = key.name;
            return keyName === eventKey;
        })[0];
        return keyMap ? self.vars.activeKeyMap[keyMap["data-key-id"]] : null;
    }

    /**
     * add letter into current text - This method just adds the given letter to the existing content (of the preview)
     * text. It manages also the position of the inserted letter
     *
     * @param {object} self
     * @param {object} key key to be inserted
     */
    function insertLetterIntoCurrentText(self, key) {
        const modValues = evaluateKeyModifiers(self, key),
            letter = modValues.val || key.val;

        if (letter) {
            self.vars.inputHandler.setTextAtCurrentPosition(letter);
        }
    }

    // #        Event Listener      # //

    /**
     * onKeyPressListener
     *
     * @param {object} self
     * @param {number} x x postion of cursor
     * @param {number} y y postion of cursor
     * @param {object} event event that is triggered by mouse
     */
    function onKeyPressListener(self, x, y, event) {
        if (event.type.indexOf('mouse') !== -1 && event.type.indexOf('touch') === -1) {
            shmi.addClass(self.element, 'hover-enabled');
        } else {
            shmi.removeClass(self.element, 'hover-enabled');
        }
        self.vars.keyPressed = event.target.getAttribute('data-key-name');

        if (self.config["password-input"]) {
            event.preventDefault();
        } else {
            shmi.addClass(event.target, 'pressed');
        }
    }

    /**
     * onKeyClickListener
     *
     * @param {object} self
     * @param {number} x x postion of cursor
     * @param {number} y y postion of cursor
     * @param {object} event event that is triggered by mouse
     */
    function onKeyClickListener(self, x, y, event) {
        var keyElement = event.target.parentNode,
            keyElementId = keyElement.getAttribute('data-key-id'),
            key = self.vars.activeKeyMap[keyElementId];
        if (keyElement && key) {
            self.vars.keyPressed = false;
            // click = press and release -> set mouse move to null
            self.vars.mouseMoveKey = null;
            fireKeyboardClick(self, key, event);
            fireTypeListeners(self, key, event); // What the heck is this function doing? HELP!
            // |^| returns the true for continue or false for preventing further actions
            // could be used as if(fireTypeListeners(self, key, event)) { fireKeyboardClick(self, key, event); }

            if (self.config["auto-key-focus"] && document.activeElement !== keyElement) {
                // focus clicked key if auto key focus is enabled and key element has a tabindex attribute
                keyElement.focus();
            }
        }
    }

    /**
     * onKeyReleaseListener
     *
     * @param {object} self
     * @param {number} x x postion of cursor
     * @param {number} y y postion of cursor
     * @param {object} event event that is triggered by mouse
     */
    function onKeyReleaseListener(self, x, y, event) {
        if (self.vars.keyPressed) {
            var el = event.target;
            shmi.removeClass(el, 'pressed');
        }
    }

    /**
     * onKeyMouseDownListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseDownListener(self, event) {
        shmi.removeClass(self.element, 'key-preview-enabled');
        var keyNameAttr = event.target.getAttribute('data-key-name') || false,
            key = self.vars.activeKeyMap[event.target.parentElement.getAttribute('data-key-id')];
        self.vars.keyPressed = keyNameAttr;
        self.vars.mouseMoveKey = null;

        if (self.vars.keyPressed && key && key.hold && !key.alternatives) {
            setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    self.vars.keyHeld = true;
                    var clickAndHoldIV = setInterval(function() {
                        if (self.vars.keyPressed === keyNameAttr) {
                            fireKeyboardClick(self, key, event);
                            fireTypeListeners(self, key, event);
                        } else {
                            clearInterval(clickAndHoldIV);
                        }
                    }, 120);
                }
            }, 1000);
        }
        if (key && key.alternatives) {
            // if alternative keys are available show them after 500ms
            self.vars.altKeysTimeout = setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    shmi.removeClass(self.element, 'key-preview-enabled');
                    shmi.addClass(event.target, 'show-alt-keys');
                }
            }, 500);
        }
    }

    /**
     * onKeyMouseUpListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseUpListener(self, event) {
        if (self.vars.keyPressed) {
            self.vars.keyPressed = false;
            var key = event.target;
            shmi.removeClass(event.target, 'pressed');
            self.vars.previewContainer.focus();
            if (self.config["auto-key-focus"] && document.activeElement !== key) {
                key.focus();
            }
            key = self.vars.activeKeyMap[key.parentElement.getAttribute('data-key-id')];
            if (key && self.vars.mouseMoveKey) {
                fireKeyboardClick(self, key, event);
                fireTypeListeners(self, key, event);
                self.vars.mouseMoveKey = null;
            }
        }
        removeAllAltKeys(self); // remove displayed alternatives
    }

    /**
     * onKeyMouseInListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseInListener(self, event) {
        if (self.vars.keyPressed) {
            const el = event.target;
            self.vars.mouseMoveKey = !self.vars.keyHeld ? el : null;
            self.vars.keyPressed = el.getAttribute('data-key-name');
            shmi.addClass(el, 'pressed');
        }
    }

    /**
     * onKeyMouseOutListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseOutListener(self, event) {
        if (self.vars.keyPressed) {
            var el = event.target;
            self.vars.mouseMoveKey = null;
            shmi.removeClass(el, 'pressed');
        }
    }

    /**
     * onKeyTouchStartListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyTouchStartListener(self, event) {
        if (self.config['key-preview']) {
            shmi.addClass(self.element, 'key-preview-enabled');
        }
        var keyNameAttr = event.target.getAttribute('data-key-name'),
            key = self.vars.activeKeyMap[event.target.parentElement.getAttribute('data-key-id')];
        self.vars.keyPressed = keyNameAttr;
        self.vars.mouseMoveKey = null;
        self.vars.activePalette.element.addEventListener('touchend', self.vars.eventListeners.touchEnd);
        self.vars.activePalette.element.addEventListener('touchmove', self.vars.eventListeners.touchMove);

        if (keyNameAttr && key && key.hold && !key.alternatives) {
            setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    self.vars.keyHeld = true;
                    var pushAndHoldIV = setInterval(function() {
                        if (self.vars.keyPressed === keyNameAttr) {
                            fireKeyboardClick(self, key, event);
                            fireTypeListeners(self, key, event);
                        } else {
                            clearInterval(pushAndHoldIV);
                        }
                    }, 120);
                }
            }, 1000);
        }
        if (key && key.alternatives) {
            // if alternative keys are available show them after 500ms
            self.vars.altKeysTimeout = setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    shmi.removeClass(self.element, 'key-preview-enabled');
                    shmi.addClass(event.target, 'show-alt-keys');
                }
            }, 500);
        }
    }

    /**
     * onKeyTouchMoveListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyTouchMoveListener(self, event) {
        event.preventDefault();
        event.stopPropagation();
        if (self.vars.keyPressed) {
            var clientY = event.touches[0].clientY,
                clientX = event.touches[0].clientX,
                keyElement = document.elementFromPoint(clientX, clientY);
            if (keyElement) {
                var keyNameAttr = keyElement.getAttribute('data-key-name');
                if (keyNameAttr && keyElement !== self.vars.dragCurrentKey) {
                    if (!self.vars.keyHeld && keyNameAttr !== self.vars.keyPressed) {
                        removeAllPressedHighlights(self);
                        if (!self.config["password-input"]) {
                            shmi.addClass(keyElement, 'pressed');
                        }
                    }
                    self.vars.dragCurrentKey = keyElement;
                    self.vars.keyPressed = keyNameAttr;
                }
            }
        }
    }

    /**
     * onKeyTouchEndListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyTouchEndListener(self, event) {
        self.vars.keyPressed = false;
        self.vars.activePalette.element.removeEventListener('touchend', self.vars.eventListeners.touchEnd);
        self.vars.activePalette.element.removeEventListener('touchmove', self.vars.eventListeners.touchMove);

        if (self.vars.dragCurrentKey && !self.vars.keyHeld) {
            removeAllPressedHighlights(self);
            self.vars.previewContainer.focus();
            if (self.config["auto-key-focus"] && document.activeElement !== event.target) {
                self.vars.dragCurrentKey.focus();
            }
            var key = self.vars.activeKeyMap[self.vars.dragCurrentKey.parentElement.getAttribute('data-key-id')];
            if (key) {
                fireKeyboardClick(self, key, event);
                fireTypeListeners(self, key, event);
            }
            self.vars.dragCurrentKey = null;
        } else {
            self.vars.keyHeld = false;
        }
        removeAllAltKeys(self); // remove all displayed alternatives
    }

    /**
     * onKeyDownListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyDownListener(self, event) {
        // do not show the key preview if a hardware keyboard event is triggered
        shmi.removeClass(self.element, 'key-preview-enabled');
        shmi.removeClass(self.element, 'hover-enabled');
        const key = getKeyByValue(self, event);
        if (key && !self.config["password-input"]) {
            // highlighting the pressed key
            shmi.addClass(key.element.firstChild, 'pressed');
            if (self.vars.previewContainer && document.activeElement !== self.vars.previewContainer) {
                if (!self.config['auto-key-focus']) {
                    self.vars.previewContainer.focus();
                }
            }
        }
        if (event.key === "Enter" && !self.config['show-enter']) {
            shmi.fire('keyboard-submit', {
                "success": true,
                "input": self.vars.currentText
            });
        }
    }

    /**
     * onKeyUpListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyUpListener(self, event) {
        if (event.key.toLowerCase() === 'escape') {
            log("Fire: Close Keyboard");
            shmi.fire('keyboard-cancel', {
                "success": false,
                "input": self.vars.currentText
            });
        }

        const key = getKeyByValue(self, event);
        if (key && !self.config["password-input"]) {
            shmi.removeClass(key.element.firstChild, 'pressed');
        }
    }

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/* handler for Keyboard slide in */
(function() {
    // this queue handler is copied from the numpad and has initially been a queue for the notification dialog
    var keyboardEvent = null,
        keyboard = null;

    function createKeyboard() {
        var keyboardConfig = {},
            iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            okTok = null,
            closeTok = null,
            enableTok = null;

        iter(keyboardEvent.detail, function(val, key) {
            keyboardConfig[key] = keyboardEvent.detail[key];
        });
        if (keyboardConfig.label) {
            keyboardConfig.label = shmi.evalString(shmi.localize(keyboardConfig.label), keyboardEvent.detail.param);
        }

        keyboard = shmi.createControl("alpha-num-keyboard", document.body, keyboardConfig, 'DIV');

        okTok = shmi.listen('keyboard-submit', function(evt) {
            okTok.unlisten();
            closeTok.unlisten();
            okTok = closeTok = null;
            deleteKeyboard();
            shmi.log("Keyboard closed, changes submited", evt);
            if (keyboardConfig.callback && typeof keyboardConfig.callback === "function") {
                keyboardConfig.callback(/*success:*/ true, /*input:*/ evt.detail.input);
            }
        });
        closeTok = shmi.listen('keyboard-cancel', function(evt) {
            closeTok.unlisten();
            okTok.unlisten();
            closeTok = okTok = null;
            deleteKeyboard();
            shmi.log("Keyboard closed, changes aborted", evt);
            if (keyboardConfig.callback && typeof keyboardConfig.callback === "function") {
                keyboardConfig.callback(/*success:*/ false, /*input:*/ null);
            }
        });

        function onKeyboardEnable() {
            if (enableTok) {
                enableTok.unlisten();
                enableTok = null;
            }
            keyboard.show();
        }

        if (keyboard.isActive()) {
            onKeyboardEnable();
        } else {
            enableTok = keyboard.listen("enable", onKeyboardEnable);
        }

        keyboard.enable();
    }

    function deleteKeyboard() {
        if (keyboard) {
            shmi.deleteControl(keyboard);
            keyboard = null;
        }
        keyboardEvent = null;
    }

    function defaultkeyboardHandler(event) {
        if (keyboard !== null) {
            return;
        }
        try {
            shmi.requires("visuals.handler.default.keyboard");
            keyboardEvent = event;
            if (keyboardEvent) {
                createKeyboard();
            }
        } catch (exc) {
            shmi.log("[Keyboard] - error trying to load controls: Keyboard\n" + exc);
        }
    }

    var module = shmi.pkg("visuals.handler.default.keyboard"),
        keyboardToken = null;

    module.register = function() {
        if (keyboardToken) {
            console.debug("Keyboard handler was active!");
            keyboardToken.unlisten();
            keyboardToken = null;
        }
        keyboardToken = shmi.listen('keyboard-request', defaultkeyboardHandler);
    };

    module.deregister = function() {
        if (!keyboardToken) {
            console.debug("Keyboard handler not active!");
        } else {
            keyboardToken.unlisten();
            keyboardToken = null;
        }
    };
}());

/**
 *
 */
(function() {
    /**
     * Displays an keyboard slide in to the user.
     * paramObj : {
     *      "key-preview": true,        // shown on touch only
     *      "auto-key-focus": false     // set tabindex="0" to key
     *      "select-box-enabled": true, // display selectbox to change language
     *      "value": null,
     *      "callback": function(Boolean success, String input) {
     *          if (success) {
     *              console.log("Keyboard successfully closed", input);
     *          } else {
     *              console.log("Keyboard aborted");
     *          }
     *      }
     * }
     *
     * @param {object} paramObj parameter object
     */
    shmi.keyboard = function(paramObj) {
        var nofListeners = shmi.fire('keyboard-request', paramObj, shmi);
        /* use native alert function if alpha num keyboard are not handled */
        if (nofListeners === 0) {
            shmi.log("[Keyboard] - Keyboard not found");
        }
    };
}());

/**
 * Complex-Table2 configuration factory for the `audit-log-viewer` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        /** @lends module:visuals.controls.iq-user-list */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Creates the `field-datagrid-col-map` property of the ct2 configuration
     * object.
     *
     * @param {DataGridAuditLog} grid Datagrid used by complex-table2.
     * @param {string[]} columnList List of columns to display.
     */
    function makeDgColumnMap(grid, columnList) {
        const out = {},
            columnMap = {
                severityIcon: "severity"
            };

        columnList.forEach((columnName) => {
            const idx = grid.getFields().findIndex((col) => col === (columnMap[columnName] ?? columnName));
            if (idx > -1) {
                out[columnName] = idx;
            }
        });

        return out;
    }

    /**
     * Extracts the names of the configured columns from a columnOrg object.
     *
     * @param {object} columnOrg
     * @returns {string[]} Array of column names used in the provided columnOrg
     *  object.
     */
    function getConfiguredColumnNames(columnOrg) {
        const columnNames = [];

        Object.entries(columnOrg).forEach(([key, columnConfig]) => {
            if (!/^col\d+$/.exec(key)) {
                return;
            } else if (!Array.isArray(columnConfig.fields)) {
                return;
            }

            columnNames.push(...columnConfig.fields);
        });

        return columnNames;
    }

    /**
     * Creates the `default-field-control-map` property of the ct2
     * configuration object.
     *
     * @param {string[]} columnList List of columns to display.
     */
    function makeFieldControlMap(columnList) {
        const types = {
            "timestampIn": "iq-date-time",
            "timestampOut": "iq-date-time"
        };
        const out = {};

        columnList.forEach((columnName) => {
            out[columnName] = {
                "ui-type": types[columnName] ?? "iq-label",
                "config": {
                    "pattern": "<%=VALUE%>"
                }
            };
        });

        out["severityIcon"] = {
            "ui-type": "iq-image-changer",
            "config": {
                "class-name": "iq-image-changer iq-variant-01",
                "template": "default/iq-image-changer.iq-variant-01",
                "default-image": "",
                "no-initial-image": true,
                "options": [
                    {
                        "label": "label",
                        "value": "info",
                        "icon-src": "pics/system/controls/iq-alarm-list/icon-info.svg",
                        "icon-title": null,
                        "icon-class": null,
                        "style": {
                            "color": "var(--webiq-ok)"
                        }
                    },
                    {
                        "label": "label",
                        "value": "warning",
                        "icon-src": "pics/system/controls/iq-alarm-list/icon-warning.svg",
                        "icon-title": null,
                        "icon-class": null,
                        "style": {
                            "color": "var(--webiq-warning)"
                        }
                    },
                    {
                        "label": "label",
                        "value": "alarm",
                        "icon-src": "pics/system/controls/iq-alarm-list/icon-alarm.svg",
                        "icon-title": null,
                        "icon-class": null,
                        "style": {
                            "color": "var(--webiq-error)"
                        }
                    }
                ],
                "scaling-mode": "contain",
                "inline-svg": true
            }
        };

        return out;
    }

    /**
     * Creates a configuration object for the complex-table2 widget used by the
     * `audit-log-viewer` widget.
     *
     * @param {DataGridAuditLog} grid Datagrid used by complex-table2.
     * @param {object} columnOrg ColumnOrg object.
     */
    module.makeAuditLogListConfig = function makeAuditLogListConfig(grid, columnOrg) {
        const columnList = getConfiguredColumnNames(columnOrg),
            searchableFields = [].filter((columnName) => columnList.includes(columnName)),
            sortableFields = ["severityIcon", "severity", "timestampIn", "timestampOut", "type"].filter((columnName) => columnList.includes(columnName));

        return {
            "table": grid.name,
            "class-name": "complex-table2 iqel-entry-table",
            "template": null,
            "field-datagrid-col-map": makeDgColumnMap(grid, columnList),
            "default-field-control-map": makeFieldControlMap(columnList),
            "default-field-headers": {
                "recordId": "${audit-log-viewer.tableHeaders.recordId}",
                "timestampIn": "${audit-log-viewer.tableHeaders.timestampIn}",
                "timestampOut": "${audit-log-viewer.tableHeaders.timestampOut}",
                "severityIcon": columnList.includes("severity") ? " " : "${audit-log-viewer.tableHeaders.severity}",
                "severity": "${audit-log-viewer.tableHeaders.severity}",
                "type": "${audit-log-viewer.tableHeaders.type}",
                "subType": "${audit-log-viewer.tableHeaders.subType}",
                "data": "${audit-log-viewer.tableHeaders.data}",
                "recordTitle": "${audit-log-viewer.tableHeaders.recordTitle}"
            },
            "default-layout": {
                "class-name": "layout-std",
                "column-org": columnOrg
            },
            "sortable-fields": sortableFields,
            "default-text-filter-fields": searchableFields,
            "quicksearch": {
                "enable": false,
                "fields": searchableFields,
                "remember": false,
                "initialFocus": false
            },
            "select-mode": "SINGLE",
            "delete-selected-rows": false,
            "show-nof-rows": true,
            "text-mode": "SINGLELINE",
            "v-scroll-options": ["SCROLLBAR", "V_SWIPE"],
            "default-nof-buffered-rows": 60,
            "buffer-size": 500
        };
    };
}());

/* eslint-disable class-methods-use-this */
(() => {
    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        /** @lends module:visuals/controls/audit-log-viewer */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Logging helpers
     */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[DataGridAuditLog]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[DataGridAuditLog]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[DataGridAuditLog]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[DataGridAuditLog]")
    };

    /**
     * @typedef {object} DataSegment
     * @property {object} data
     * @property {number} refCount
     * @property {number} startIndex
     * @property {number} size
     */

    /**
     * @typedef {object} Subscriber
     * @property {number} id
     * @property {number} offset
     * @property {number} size
     * @property {number} numRequests
     * @property {DataSegment[]} segments
     * @property {function} onChange
     */

    /**
     * @typedef {object} AuditApiInterface
     * @property {function} query
     * @property {function} getUserCache
     */

    let nextId = 1;

    /**
     * Returns a unique id
     *
     * @private
     * @returns {number} Unique id
     */
    function generateId() {
        return nextId++;
    }

    /**
     * Create a datagrid row from an audit log record.
     *
     * @private
     * @param {object} record
     * @returns {object[]}
     */
    function makeRowDataFromRecord(record) {
        return [
            {
                type: shmi.c("TYPE_STRING"),
                value: record.record_id,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_FLOAT"),
                value: record.timestamp_in / 10000000,
                min: 0,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_FLOAT"),
                value: record.timestamp_out === null ? null : record.timestamp_out / 10000000,
                min: 0,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: record.severity,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: record.type,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: record.sub_type,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: record.username ? `$<${record.user_realm}>:${record.username}` : null,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: record.data,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: record.sub_type?.length ? `\${audit-log-viewer.recordTitle.${record.type}.${record.sub_type}}` : `\${audit-log-viewer.recordTitle.${record.type}}`,
                min: Number.NaN,
                max: Number.NaN
            }
        ];
    }

    /**
     *
     * @param {DataGridAuditLog} self Reference to a DataGridAuditLog.
     */
    async function loadUserCache(self) {
        if (self.userCache) {
            return;
        }

        const result = await self.interface.getUserCache().catch(() => []),
            guidMap = {};

        result.forEach((entry) => guidMap[entry.user_id] = entry.username);

        self.userCache = {
            guidMap,
            raw: result
        };
    }

    /**
     * Inserts a row into the datagrid and creates new virtual items if
     * necessary.
     *
     * @private
     * @param {DataGridAuditLog} self Reference to a DataGridAuditLog.
     * @param {number} rowId ROWID of the row
     * @param {object[]} rowData Rowdata
     */
    function makeRow(self, rowId, rowData) {
        const im = shmi.visuals.session.ItemManager;

        shmi.checkArg("self", self, "shmi.visuals.controls.audit-log-viewer.DataGridAuditLog");
        shmi.checkArg("rowId", rowId, "number");
        shmi.checkArg("rowData", rowData, "array");

        self.data[rowId] = rowData;

        rowData.forEach(function(cell, i) {
            const vItemName = `${this.prefix}${rowId}:${i}`,
                cellItem = im.getItem(vItemName);

            if (cellItem && cellItem.initialized) {
                cell.item = cellItem;
                cellItem.setValue(cell.value);
            } else {
                cell.item = shmi.createVirtualItem(vItemName, cell.type, cell.min, cell.max, cell.value,
                    (value, type, name) => {
                        const row = this.getRowData(rowId);

                        if (row && row[i]) {
                            row[i].value = value;
                        }
                    }
                );
                cell.item.setDataGridInfo(self.name, rowId, i);
            }
        }, self);
    }

    /**
     * Fetches data for the first subscriber in the update queue.
     *
     * @param {DataGridAuditLog} self
     */
    async function loadNext(self) {
        if (self.updateQueue.length === 0) {
            // No more updates. We're done.
            self.loading = false;
            return;
        }

        if (self.userCache === null) {
            await loadUserCache(self);
        }

        /** @type {string} */
        const subscriberId = self.updateQueue.shift(),
            subscriber = self.subscribers[subscriberId];

        if (!subscriber) {
            // Subscriber not found.
            loadNext(self);
            return;
        }

        let numRequests = subscriber.numRequests;
        subscriber.numRequests = 0;

        try {
            const queryResult = await self.interface.query({
                ...self.filter,
                sort: self.sortBy ? [self.sortBy] : [],
                offset: subscriber.offset,
                limit: subscriber.size
            });

            queryResult.records.forEach((record, idx) => {
                self.rowObjectData[subscriber.offset + idx] = record;
                makeRow(self, subscriber.offset + idx, makeRowDataFromRecord({
                    ...record,
                    username: self.getUsername(record.user_realm, record.user_id)
                }));
            });

            self.totalRows = queryResult.total_records;

            for (; numRequests > 0; numRequests--) {
                subscriber.onChange({
                    status: "OK",
                    offset: subscriber.offset,
                    size: subscriber.size,
                    totalRows: self.totalRows
                });
            } // for ( ... )
        } catch (e) {
            logger.error("Failed to request data from backend:", e);

            for (; numRequests > 0; numRequests--) {
                subscriber.onChange({
                    status: "FAILED",
                    totalRows: self.totalRows ?? 0
                });
            } // for ( ... )
        }

        loadNext(self);
    }

    /**
     * Queues fetching new data for a subscriber. If the subscriber is already
     * queued it is not inserted into the queue again.
     *
     * @param {DataGridAuditLog} self
     * @param {string} subscriberId Id of the subscriber to update.
     */
    function queueSubscriberUpdate(self, subscriberId) {
        if (!self.updateQueue.includes(subscriberId)) {
            self.updateQueue.push(subscriberId);
        }

        if (!self.loading) {
            self.loading = true;
            loadNext(self);
        }
    }

    class DataGridAuditLog {
        /**
         * Constructs a new DataGridAuditLog.
         *
         * @param {string} name Name of the datagrid.
         * @param {AuditApiInterface} apiInterface Interface to the audit API.
         */
        constructor(name, apiInterface) {
            this.name = name;
            /** @type {Record<string, Subscriber>} */
            this.subscribers = {};
            this.prefix = `virtual:grid:${name}:`;
            /** @type {AuditApiInterface} */
            this.interface = apiInterface;
            this.filter = {};
            this.sortBy = null;
            /** @type {string[]} */
            this.updateQueue = [];
            /** @type {boolean} */
            this.loading = false;
            /** @type {Record<number, object[]>} */
            this.data = {};
            /** @type {Record<number, object>} */
            this.rowObjectData = {};
            /** @type {object|null} */
            this.userCache = null;
        }

        init() {}

        /**
         * subscribePage - open subscription datagrid row range
         *
         * @param  {number} offset   row offset of result data
         * @param  {number} size     page size of data to load
         * @param  {function} onChange callback to run when datagrid changes
         * @return {object} subscription token
         */
        subscribePage(offset, size, onChange) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("size", size, "number");
            shmi.checkArg("onChange", onChange, "function");

            const newSubscriber = {
                id: generateId(),
                offset: offset,
                size: size,
                segments: [],
                numRequests: 1,
                onChange: onChange
            };

            this.subscribers[newSubscriber.id] = newSubscriber;

            queueSubscriberUpdate(this, newSubscriber.id);

            return {
                id: newSubscriber.id,
                prefix: this.prefix
            };
        }

        /**
         * unsubscribe - return / disable active subscription to datagrid range
         *
         * @param  {number} subID subscription ID
         */
        unsubscribe(subID) {
            const im = shmi.requires("visuals.session.ItemManager"),
                iter = shmi.requires("visuals.tools.iterate.iterateObject");

            shmi.checkArg("subID", subID, "number");

            delete this.subscribers[subID];

            /* if no clients left ... */
            if (Object.keys(this.subscribers).length === 0) {
                iter(this.data, (row) => {
                    row.forEach((cell) => im.removeItem(cell.item.name));
                });
                this.data = {};
                this.rowObjectData = {};
            }
        }

        /**
         * getFields - get array of datagrid field names
         *
         * @return {string[]} array of field names
         */
        getFields() {
            return ["recordId", "timestampIn", "timestampOut", "severity", "type", "subType", "username", "data", "recordTitle"];
        }
        /**
         * getIndexFields - get indices of index fields
         *
         * @return {number[]}  array of field indices
         */
        getIndexFields() {
            return [0];
        }

        /**
         * getRowIndex - get values of index fields for specified row-ID
         *
         * @param  {number} rowId datagrid-row ID
         * @return {array} array of index field values
         */
        getRowIndex(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            const rowData = this.getRowData(rowId);

            if (!rowData) {
                return null;
            }

            return this.getIndexFields().map((el) => rowData[el].value);
        }

        /**
         * searchIndexRowId - get datagrid-row ID of specified index field values
         *
         * @param  {number[]} rowIndex array of index field values
         * @return {number} datagrid-row ID matching index values or -1 if not found
         */
        searchIndexRowId(rowIndex) {
            const fields = this.getIndexFields();
            let rowId = -1;

            shmi.checkArg("rowIndex", rowIndex, "array");

            Object.keys(this.data).some((id) => {
                id = parseInt(id);
                if (this.searchIndexFields(id, fields, rowIndex)) {
                    rowId = id;

                    return true;
                }

                return false;
            });

            return rowId;
        }

        /**
         * searchIndexFields - test if index fields of row match specified index values
         *
         * @param  {number} row datagrid-row ID
         * @param  {number[]} fields index field indices
         * @param  {number[]} rowIndex index field values
         * @return {boolean} `true` if found, `false` else
         */
        searchIndexFields(row, fields, rowIndex) {
            shmi.checkArg("row", row, "number");
            shmi.checkArg("fields", fields, "array");
            shmi.checkArg("rowIndex", rowIndex, "array");

            return fields.length > 0 && rowIndex.every((cell, idx) => cell === this.data[row]?.[fields[idx]]?.value);
        }

        /**
         * setOffset - set offset of subscribed datagrid range
         *
         * @param  {number} offset datagrid result offset
         * @param  {number} subID  active subscription ID
         */
        setOffset(offset, subID) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("subID", subID, "number");

            if (this.subscribers[subID] !== undefined) {
                this.subscribers[subID].offset = offset;
                this.subscribers[subID].numRequests++;
                queueSubscriberUpdate(this, subID);
            }
        }

        /**
         * returns row IDs of currently display rows.
         *
         * @param {number} subID subscriber id
         * @returns {number[]}
         */
        getCurrentIDs(subID) {
            shmi.checkArg("subID", subID, "number");

            var sub = this.subscribers[subID];

            if (sub === undefined) {
                return [];
            }

            return Object.keys(this.data).map(function(rowID) {
                return parseInt(rowID);
            }).filter(function(rowID) {
                return sub.offset <= rowID && rowID < sub.offset + sub.size;
            });
        }

        /**
         * returns all row IDs of the datagrid
         *
         * @returns {number[]}
         */
        getAllIDs() {
            return Object.keys(this.data);
        }

        /**
         *  retrieve values of a row as array
         *
         * @param {number} rowId row-ID of the data-row to fetch
         * @returns {object[]} row-data array
         */
        getRowData(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            return this.data[rowId] ?? null;
        }

        getRowDataRaw(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            return this.rowObjectData[rowId] ?? null;
        }

        /**
         * Retrieves the number of rows in this DataGrid.
         *
         * @returns {number} number of rows
         */
        getRowCount() {
            return this.totalRows;
        }

        refresh() {
            Object.entries(this.subscribers).forEach(([subscriberId, subscriber]) => {
                subscriber.numRequests++;
                queueSubscriberUpdate(this, subscriberId);
            });
        }

        /**
         * Returns the username for the given realm and user id.
         *
         * @param {string} userRealm
         * @param {string} userId
         * @returns {?string}
         */
        getUsername(userRealm, userId) {
            if (!userRealm || !userId) {
                return null;
            }

            return this.userCache?.raw.find((cacheEntry) => (cacheEntry.user_realm === userRealm && cacheEntry.user_id === userId))?.username ?? null;
        }

        /**
         * Returns a map of user guids to their usernames.
         *
         * @returns {object}
         */
        getGuidMap() {
            return this.userCache?.guidMap ?? {};
        }

        /** Stub */
        setFilter() { }
        /** Stub */
        setFilterChaining() { }

        getFilters() {
            return [];
        }

        clearFilter() {
            this.filter = {};
        }

        setFilterEx(filterSettings) {
            const newFilter = {
                timestamp: {
                    mode: "AND",
                    clauses: [
                        {
                            comp: ">=",
                            timestamp: filterSettings.startTime * 10000
                        },
                        {
                            comp: "<=",
                            timestamp: filterSettings.endTime * 10000
                        }
                    ]
                },
                type: {
                    mode: "AND",
                    clauses: []
                },
                sub_type: {
                    mode: "AND",
                    clauses: []
                },
                data: {
                    mode: filterSettings.attributeRows.length === 0 ? "AND" : (filterSettings.junctionMode ?? "AND"),
                    clauses: filterSettings.attributeRows.map((row) => ({
                        comp: row.comparisonType,
                        path: row.attributeName,
                        value: row.comparisonValue
                    }))
                }
            };

            filterSettings.filters?.forEach((filter) => {
                newFilter.type.clauses.push({
                    comp: "=",
                    type: filter.type
                });
                if (filter.subType) {
                    newFilter.sub_type.clauses.push({
                        comp: "=",
                        sub_type: filter.subType
                    });
                }
            });

            if (newFilter.type.clauses.length) {
                newFilter.type.mode = "OR";
            }

            if (newFilter.sub_type.clauses.length) {
                newFilter.sub_type.mode = "OR";
            }

            this.filter = newFilter;
        }

        sort(col, order) {
            const columnName = this.getFields()[col];
            switch (columnName) {
            case "timestampIn":
                this.sortBy = {
                    column: "timestamp_in",
                    order
                };
                break;
            case "timestampOut":
                this.sortBy = {
                    column: "timestamp_out",
                    order
                };
                break;
            case "severityIcon":
                this.sortBy = {
                    column: "severity",
                    order
                };
                break;
            case "type":
            case "severity":
                this.sortBy = {
                    column: columnName,
                    order
                };
                break;
            default:
                this.sortBy = null;
            }

            this.refresh();
        }
    }

    module.DataGridAuditLog = DataGridAuditLog;
})();

/**
 * Dialog for editing the audit log viewer's filter.
 */
(function() {
    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-log-viewer-filter-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * Returns the current value typecasted to the type expected by the
     * attribute.
     *
     * @param {*} state
     * @param {object} rowData Object of the row's entry.
     * @param {object[]} options Array of options available.
     * @returns {any}
     */
    function convertRowValue(state, rowData, options) {
        const currentOption = options.find(({ value }) => value === rowData.attributeName);
        if (!currentOption) {
            return state.data.rawValue;
        }

        switch (currentOption.type) {
        case "auto": {
            const number = Number(state.data.rawValue);
            if (state.data.rawValue === null) {
                return null;
            } else if (state.data.rawValue === "") {
                return "";
            } else if (!isNaN(number)) {
                return number;
            } else if (["no", "false"].includes(state.data.rawValue.toLocaleLowerCase())) {
                return 0;
            } else if (["yes", "true"].includes(state.data.rawValue.toLocaleLowerCase())) {
                return 1;
            }
            return state.data.rawValue;
        }
        case "boolean":
            if (state.data.rawValue === null) {
                return 0;
            } else if (typeof state.data.rawValue === "number") {
                return !!state.data.rawValue;
            } else if (typeof state.data.rawValue === "string") {
                return ["n", "no", "false", "0", ""].includes(state.data.rawValue.toLocaleLowerCase()) ? 0 : 1;
            } else {
                return 1;
            }
        case "number":
            return parseFloat(state.data.rawValue);
        case "string":
        default:
            return state.data.rawValue;
        }
    }

    /**
     * Returns a random string length 9.
     *
     * @returns {string}
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * Initializes the datagrid if it doesnt exist yet.
     *
     * @param {*} state
     */
    function initDataGridItem(state) {
        if (state.data.dgItemId) {
            return;
        }

        const dgm = shmi.requires("visuals.session.DataGridManager");

        do {
            state.data.dgItemId = `${MODULE_NAME}-item-${getRandomId()}`;
        } while (dgm.getGrid(state.data.dgItemId) !== null);

        dgm.grids[state.data.dgItemId] = new module.DataGridItem(state.data.dgItemId, {
            query: (params) => shmi.visuals.session.ConnectSession.requestPromise("item.list", params)
        });
    }

    /**
     * Instantiates the correct input widget for the currently selected option.
     *
     * @param {*} state
     * @param {object} rowData Object of the row's entry.
     * @param {object[]} options Array of options available.
     */
    function initRowValue(state, rowData, options) {
        const currentOption = options.find(({ value }) => value === rowData.attributeName),
            selectedType = currentOption?.type ?? "string",
            valueHolder = state.getInstance("comparisonValueHolder"),
            controlsToAdd = [];

        // Clear all children
        valueHolder.controls.slice().forEach((ctrl) => ctrl.delete(true));

        if (selectedType === "item") {
            initDataGridItem(state);
            controlsToAdd.push({
                "ui": "iq-searchbar",
                "config": {
                    "class-name": "iq-searchbar iq-variant-01 item-searchbar",
                    "name": "comparisonValue",
                    "label": null,
                    "datagrid": state.data.dgItemId,
                    "search-column": "itemAlias",
                    "show-results": true,
                    "result-columns": ["itemAlias"],
                    "partial-word-search": true,
                    "_controllers_": getControllerConfig("comparisonValueSearchbar")
                }
            });
        } else {
            controlsToAdd.push({
                "ui": "iq-input-field",
                "config": {
                    "class-name": "iq-input-field iq-variant-01",
                    "name": "comparisonValue",
                    "label": null,
                    "unit-text": null,
                    "_controllers_": getControllerConfig("comparisonValueInput")
                }
            });
        }

        valueHolder.addControl(controlsToAdd, (err, controls) => {
            controls.forEach((ctrl) => ctrl.setValue(rowData.comparisonValue ?? ""));
        });
    }

    /**
     * Creates the controller configuration for a new row in the filter table.
     * The controller is responsible for removing the filter object from the
     * array of rows. As such the row array must contain the filter object.
     *
     * @param {object} rowData Object of the row's entry.
     * @param {array} rows Array of the filter's rows. Must contain `rowData`
     *  and may be mutated.
     * @param {object[]} options Array of options available.
     * @param {function} onChange Function that is called when a row is
     *  added or removed.
     * @returns {object}
     */
    function makeRowController(rowData, rows, options, onChange) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                rowContainer: {
                    ui: "container"
                },
                attributeName: {
                    ui: "iq-select-box",
                    events: ["change"]
                },
                comparisonType: {
                    ui: "iq-select-box",
                    events: ["change"]
                },
                comparisonValueHolder: {
                    ui: "container"
                },
                comparisonValueInput: {
                    ui: "iq-input-field",
                    events: ["change"],
                    optional: true
                },
                comparisonValueSearchbar: {
                    ui: "iq-searchbar",
                    events: ["change"],
                    optional: true
                },
                btnDelete: {
                    ui: "iq-button",
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {
                Object.entries(rowData).forEach(([slotName, slotValue]) => {
                    if (slotValue !== undefined && slotValue !== null) {
                        state.getInstance(slotName)?.setValue(slotValue);
                    }
                });
                initRowValue(state, rowData, options);
                onChange();
            },
            onDisable: function(state) {
                const rowIdx = rows.findIndex((row) => row === rowData);
                if (rowIdx > -1) {
                    rows.splice(rowIdx, 1);
                }

                if (state.data.dgItemId) {
                    const dgm = shmi.requires("visuals.session.DataGridManager");
                    delete dgm.grids[state.data.dgItemId];
                    state.data.dgItemId = null;
                }
                onChange();
            },
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "btnDelete":
                    shmi.deleteControl(state.getInstance("rowContainer"));
                    break;
                case "attributeName":
                    rowData[slot] = event.detail.value;
                    rowData.comparisonValue = convertRowValue(state, rowData, options);
                    initRowValue(state, rowData, options);
                    break;
                case "comparisonType":
                    rowData[slot] = event.detail.value;
                    break;
                case "comparisonValueInput":
                case "comparisonValueSearchbar":
                    state.data.rawValue = event.detail.value;
                    rowData.comparisonValue = convertRowValue(state, rowData, options);
                    break;
                default:
                }
            },
            data: {
                rowData,
                rawValue: rowData.comparisonValue ?? null
            }
        };
    }

    /**
     * Generates a widget tree configuration for a new row of the filter table
     * to be passed to `BaseControl.addControl()`.
     *
     * @param {object} rowData Object of the row's entry.
     * @param {array} rows Array of the filter's rows. Must contain `rowData`
     *  and may be mutated.
     * @param {array} [options] Available options selectable via the select box.
     *  This parameter is passed through to the iq-select-box widget.
     * @param {function} onChange Function that is called when a row is
     *  added or removed.
     * @returns {object}
     */
    function makeFilterRow(rowData, rows, options, onChange) {
        return {
            "ui": "container",
            "config": {
                "class-name": "iq-container filter-grid-row",
                "type": "iqflex",
                "flex-orientation": "row",
                "flex-wrap": false,
                "_controllers_": getControllerConfig("rowContainer")
            },
            "controller": makeRowController(rowData, rows, options ?? [], onChange),
            "children": [
                {
                    "ui": "iq-select-box",
                    "config": {
                        "class-name": "iq-select-box iq-variant-01 flex-1 attribute-name",
                        "template": "default/iq-select-box.iq-variant-01",
                        "name": "attributeName",
                        "options": options ?? [],
                        "selected": 0,
                        "label": null,
                        "unit-text": null,
                        "_controllers_": getControllerConfig("attributeName")
                    }
                },
                {
                    "ui": "iq-select-box",
                    "config": {
                        "class-name": "iq-select-box iq-variant-01 flex-1 comparison-type",
                        "template": "default/iq-select-box.iq-variant-01",
                        "name": "comparisonType",
                        "options": [
                            {
                                "label": "=",
                                "value": "="
                            },
                            {
                                "label": "<>",
                                "value": "<>"
                            },
                            {
                                "label": "<",
                                "value": "<"
                            },
                            {
                                "label": "<=",
                                "value": "<="
                            },
                            {
                                "label": ">",
                                "value": ">"
                            },
                            {
                                "label": ">=",
                                "value": ">="
                            },
                            {
                                "label": "includes",
                                "value": "includes"
                            }
                        ],
                        "selected": 0,
                        "label": null,
                        "unit-text": null,
                        "_controllers_": getControllerConfig("comparisonType")
                    }
                },
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container comparison-value",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-secondary-align": "stretch",
                        "flex-wrap": false,
                        "_controllers_": getControllerConfig("comparisonValueHolder")
                    }
                },
                {
                    "ui": "iq-button",
                    "config": {
                        "class-name": "iq-button iq-variant-01 delete-button",
                        "template": "default/iq-button.iq-variant-01",
                        "tooltip": "Remove entry",
                        "label": null,
                        "show-icon": true,
                        "show-text": false,
                        "icon-src": "pics/system/controls/iq-user-controller/remove-dark.svg",
                        "inline-svg-icon": true,
                        "_controllers_": getControllerConfig("btnDelete")
                    }
                }
            ]
        };
    }

    /**
     * Appends a new attribute filter row to a container.
     *
     * @param {BaseControl} parentContainer Container control to append the new
     *  row to.
     * @param {array} options Available attributes to choose from. Passed
     *  through to the iq-select-box widget.
     * @param {object} rowData Object of the row's entry.
     * @param {array} rows Array of the filter's rows. Must contain `rowData`
     *  and may be mutated.
     * @param {function} onChange Function that is called when a row is
     *  added or removed.
     */
    function createAttributeRow(parentContainer, options, rowData, rows, onChange) {
        const rowLayout = makeFilterRow(rowData, rows, options, onChange);

        parentContainer.addControl(rowLayout, (err, controls) => {});
    }

    // Only export the "createAttributeRow" function if it hasn't already been
    // provided by a different layout module.
    if (!module.createAttributeRow) {
        module.createAttributeRow = createAttributeRow;
    }

    // MODULE CODE - END

    fLog("module loaded");
})();

/**
 * Dialog for editing the audit log viewer's filter.
 */
(function() {
    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-log-viewer-filter-attribute-select-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * Creates a controller configuration for a new dialog.
     *
     * @param {function} resolve Function called upon user confirmation or
     *  cancellation.
     * @param {object[]} [options] Filter data to pre-populate the dialog
     *  with.
     * @returns {object}
     */
    function makeDialogController(resolve, options = []) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                ...Object.fromEntries(options.map((option, idx) => [`selectAttribute${idx}`, {
                    ui: "iq-button",
                    events: ["click"]
                }]))
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {
            },
            onDisable: function(state) {},
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                default:
                    if (!state.data.resolved) {
                        options.forEach((attributeOption, idx) => {
                            if (slot === `selectAttribute${idx}`) {
                                state.data.resolved = true;
                                resolve(attributeOption);
                                state.getInstance("dialog").hide();
                            }
                        });
                    }
                    break;
                }
            },
            data: {}
        };
    }

    /**
     * Generates a widget tree configuration for the dialog to be passed to
     * `BaseControl.addControl()`.
     *
     * @param {function} resolve Function called upon user confirmation or
     *  cancellation.
     * @param {object[]} [options] Filter data to pre-populate the dialog
     *  with.
     * @returns {object}
     */
    function makeDialogLayout(resolve, options = []) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "auditLogViewerFilterAttributeDialog",
                "class-name": "dialog-box option audit-log-viewer-filter-dialog",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${audit-log-viewer.attributeDialog.title}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(resolve, options),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container grid-spacing main-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": options.map((attributeOption, idx) => ({
                        "ui": "iq-button",
                        "config": {
                            "class-name": "iq-button iq-variant-01",
                            "template": "default/iq-button.iq-variant-01",
                            "label": attributeOption.label ?? "",
                            "_controllers_": getControllerConfig(`selectAttribute${idx}`)
                        }
                    }))
                }
            ]
        };
    }

    /**
     * Creates a dialog prompting the user for a filter configuration.
     *
     * @param {string[]} [options] Filter data to pre-populate the dialog
     *  with.
     * @returns {Promise} Promise that is resolved with the filter
     *  configuration on user confirmation or `null` on user cancellation.
     */
    function createFilterAttributeSelectDialog(options = []) {
        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(resolve, options);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                }
            });
        });
    }

    // Only export the "createFilterAttributeSelectDialog" function if it
    // hasn't already been provided by a different layout module.
    if (!module.createFilterAttributeSelectDialog) {
        module.createFilterAttributeSelectDialog = createFilterAttributeSelectDialog;
    }

    // MODULE CODE - END

    fLog("module loaded");
})();

/* eslint-disable class-methods-use-this */
(() => {
    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        /** @lends module:visuals/controls/audit-log-viewer */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Logging helpers
     */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[DataGridItem]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[DataGridItem]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[DataGridItem]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[DataGridItem]")
    };

    /**
     * @typedef {object} DataSegment
     * @property {object} data
     * @property {number} refCount
     * @property {number} startIndex
     * @property {number} size
     */

    /**
     * @typedef {object} Subscriber
     * @property {number} id
     * @property {number} offset
     * @property {number} size
     * @property {DataSegment[]} segments
     * @property {function} onChange
     */

    /**
     * @typedef {object} AuditApiInterface
     * @property {function} query
     * @property {function} getUserCache
     */

    let nextId = 1;

    /**
     * Returns a unique id
     *
     * @private
     * @returns {number} Unique id
     */
    function generateId() {
        return nextId++;
    }

    /**
     * @param {DataGridItem} self
     */
    function buildFilter(self) {
        const [itemAlias] = self.getFields();
        let filter = undefined;

        if (self.filter[itemAlias]) {
            filter = {
                "mode": "OR",
                "clauses": self.filter[itemAlias].map((value) => ({
                    "clauses": [
                        {
                            "operator": "like",
                            "value": value
                        }
                    ],
                    "column": "alias",
                    "location": "property",
                    "mode": "OR"
                }))
            };
        }

        return { filter };
    }

    /**
     * Create a datagrid row from an audit log record.
     *
     * @private
     * @param {object} record
     * @returns {object[]}
     */
    function makeRowDataFromRecord(record) {
        return [
            {
                type: shmi.c("TYPE_STRING"),
                value: record.item_alias ?? record.alias,
                min: Number.NaN,
                max: Number.NaN
            }
        ];
    }

    /**
     * Inserts a row into the datagrid and creates new virtual items if
     * necessary.
     *
     * @private
     * @param {DataGridItem} self Reference to a DataGridItem.
     * @param {number} rowId ROWID of the row
     * @param {object[]} rowData Rowdata
     */
    function makeRow(self, rowId, rowData) {
        const im = shmi.visuals.session.ItemManager;

        shmi.checkArg("self", self, "shmi.visuals.controls.audit-log-viewer.DataGridItem");
        shmi.checkArg("rowId", rowId, "number");
        shmi.checkArg("rowData", rowData, "array");

        self.data[rowId] = rowData;

        rowData.forEach(function(cell, i) {
            const vItemName = `${this.prefix}${rowId}:${i}`,
                cellItem = im.getItem(vItemName);

            if (cellItem && cellItem.initialized) {
                cell.item = cellItem;
                cellItem.setValue(cell.value);
            } else {
                cell.item = shmi.createVirtualItem(vItemName, cell.type, cell.min, cell.max, cell.value,
                    (value, type, name) => {
                        const row = this.getRowData(rowId);

                        if (row && row[i]) {
                            row[i].value = value;
                        }
                    }
                );
                cell.item.setDataGridInfo(self.name, rowId, i);
            }
        }, self);
    }

    /**
     * Fetches data for the first subscriber in the update queue.
     *
     * @param {DataGridAuditLog} self
     */
    async function loadNext(self) {
        if (self.updateQueue.length === 0) {
            // No more updates. We're done.
            self.loading = false;
            return;
        }

        /** @type {string} */
        const subscriberId = self.updateQueue.shift(),
            subscriber = self.subscribers[subscriberId];

        if (!subscriber) {
            // Subscriber not found.
            loadNext(self);
            return;
        }

        try {
            const queryResult = await self.interface.query({
                ...buildFilter(self),
                sort: self.sortBy ? [self.sortBy] : [],
                offset: subscriber.offset,
                limit: subscriber.size
            });

            queryResult.items.forEach((record, idx) => {
                self.rowObjectData[subscriber.offset + idx] = record;
                makeRow(self, subscriber.offset + idx, makeRowDataFromRecord({
                    ...record,
                    username: self.getUsername(record.user_realm, record.user_id)
                }));
            });

            self.totalRows = queryResult.total_count;
            subscriber.onChange({
                status: "OK",
                offset: subscriber.offset,
                size: subscriber.size,
                totalRows: self.totalRows
            });
        } catch (e) {
            logger.error("Failed to request data from backend:", e);
            subscriber.onChange({
                status: "FAILED",
                totalRows: self.totalRows ?? 0
            });
        }

        loadNext(self);
    }

    /**
     * Queues fetching new data for a subscriber. If the subscriber is already
     * queued it is not inserted into the queue again.
     *
     * @param {DataGridAuditLog} self
     * @param {string} subscriberId Id of the subscriber to update.
     */
    function queueSubscriberUpdate(self, subscriberId) {
        if (!self.updateQueue.includes(subscriberId)) {
            self.updateQueue.push(subscriberId);
        }

        if (!self.loading) {
            self.loading = true;
            loadNext(self);
        }
    }

    class DataGridItem {
        /**
         * Constructs a new DataGridAuditLog.
         *
         * @param {string} name Name of the datagrid.
         * @param {AuditApiInterface} apiInterface Interface to the audit API.
         */
        constructor(name, apiInterface) {
            this.name = name;
            /** @type {Record<string, Subscriber>} */
            this.subscribers = {};
            this.prefix = `virtual:grid:${name}:`;
            /** @type {AuditApiInterface} */
            this.interface = apiInterface;
            this.filter = {};
            this.sortBy = null;
            /** @type {string[]} */
            this.updateQueue = [];
            /** @type {boolean} */
            this.loading = false;
            /** @type {Record<number, object[]>} */
            this.data = {};
            /** @type {Record<number, object>} */
            this.rowObjectData = {};
            /** @type {object|null} */
            this.userCache = null;
        }

        init() {}

        /**
         * subscribePage - open subscription datagrid row range
         *
         * @param  {number} offset   row offset of result data
         * @param  {number} size     page size of data to load
         * @param  {function} onChange callback to run when datagrid changes
         * @return {object} subscription token
         */
        subscribePage(offset, size, onChange) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("size", size, "number");
            shmi.checkArg("onChange", onChange, "function");

            const newSubscriber = {
                id: generateId(),
                offset: offset,
                size: size,
                segments: [],
                onChange: onChange
            };

            this.subscribers[newSubscriber.id] = newSubscriber;

            queueSubscriberUpdate(this, newSubscriber.id);

            return {
                id: newSubscriber.id,
                prefix: this.prefix
            };
        }

        /**
         * unsubscribe - return / disable active subscription to datagrid range
         *
         * @param  {number} subID subscription ID
         */
        unsubscribe(subID) {
            const im = shmi.requires("visuals.session.ItemManager"),
                iter = shmi.requires("visuals.tools.iterate.iterateObject");

            shmi.checkArg("subID", subID, "number");

            delete this.subscribers[subID];

            /* if no clients left ... */
            if (Object.keys(this.subscribers).length === 0) {
                iter(this.data, (row) => {
                    row.forEach((cell) => im.removeItem(cell.item.name));
                });
                this.data = {};
                this.rowObjectData = {};
            }
        }

        /**
         * getFields - get array of datagrid field names
         *
         * @return {string[]} array of field names
         */
        getFields() {
            return ["itemAlias"];
        }
        /**
         * getIndexFields - get indices of index fields
         *
         * @return {number[]}  array of field indices
         */
        getIndexFields() {
            return [0];
        }

        /**
         * getRowIndex - get values of index fields for specified row-ID
         *
         * @param  {number} rowId datagrid-row ID
         * @return {array} array of index field values
         */
        getRowIndex(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            const rowData = this.getRowData(rowId);

            if (!rowData) {
                return null;
            }

            return this.getIndexFields().map((el) => rowData[el].value);
        }

        /**
         * searchIndexRowId - get datagrid-row ID of specified index field values
         *
         * @param  {number[]} rowIndex array of index field values
         * @return {number} datagrid-row ID matching index values or -1 if not found
         */
        searchIndexRowId(rowIndex) {
            const fields = this.getIndexFields();
            let rowId = -1;

            shmi.checkArg("rowIndex", rowIndex, "array");

            Object.keys(this.data).some((id) => {
                id = parseInt(id);
                if (this.searchIndexFields(id, fields, rowIndex)) {
                    rowId = id;

                    return true;
                }

                return false;
            });

            return rowId;
        }

        /**
         * searchIndexFields - test if index fields of row match specified index values
         *
         * @param  {number} row datagrid-row ID
         * @param  {number[]} fields index field indices
         * @param  {number[]} rowIndex index field values
         * @return {boolean} `true` if found, `false` else
         */
        searchIndexFields(row, fields, rowIndex) {
            shmi.checkArg("row", row, "number");
            shmi.checkArg("fields", fields, "array");
            shmi.checkArg("rowIndex", rowIndex, "array");

            return fields.length > 0 && rowIndex.every((cell, idx) => cell === this.data[row]?.[fields[idx]]?.value);
        }

        /**
         * setOffset - set offset of subscribed datagrid range
         *
         * @param  {number} offset datagrid result offset
         * @param  {number} subID  active subscription ID
         */
        setOffset(offset, subID) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("subID", subID, "number");

            if (this.subscribers[subID] !== undefined) {
                this.subscribers[subID].offset = offset;
                queueSubscriberUpdate(this, subID);
            }
        }

        /**
         * returns row IDs of currently display rows.
         *
         * @param {number} subID subscriber id
         * @returns {number[]}
         */
        getCurrentIDs(subID) {
            const self = this;

            shmi.checkArg("subID", subID, "number");

            const sub = this.subscribers[subID];

            if (sub === undefined) {
                return [];
            }

            return Object.keys(this.data).map(function(rowID) {
                return parseInt(rowID);
            }).filter(function(rowID) {
                return sub.offset <= rowID && rowID < Math.min(sub.offset + sub.size, self.totalRows);
            });
        }

        /**
         * returns all row IDs of the datagrid
         *
         * @returns {number[]}
         */
        getAllIDs() {
            return Object.keys(this.data);
        }

        /**
         *  retrieve values of a row as array
         *
         * @param {number} rowId row-ID of the data-row to fetch
         * @returns {object[]} row-data array
         */
        getRowData(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            return this.data[rowId] ?? null;
        }

        getRowDataRaw(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            return this.rowObjectData[rowId] ?? null;
        }

        /**
         * Retrieves the number of rows in this DataGrid.
         *
         * @returns {number} number of rows
         */
        getRowCount() {
            return this.totalRows;
        }

        refresh() {
            Object.keys(this.subscribers).forEach((subscriberId) => queueSubscriberUpdate(this, subscriberId));
        }

        /**
         * Returns the username for the given realm and user id.
         *
         * @param {string} userRealm
         * @param {string} userId
         * @returns {?string}
         */
        getUsername(userRealm, userId) {
            if (!userRealm || !userId) {
                return null;
            }

            return this.userCache?.raw.find((cacheEntry) => (cacheEntry.user_realm === userRealm && cacheEntry.user_id === userId))?.username ?? null;
        }

        /**
         * Returns a map of user guids to their usernames.
         *
         * @returns {object}
         */
        getGuidMap() {
            return this.userCache?.guidMap ?? {};
        }

        /** Stub */
        setFilter(col, expression) {
            this.filter[this.getFields()[col]] = expression;
            this.refresh();
        }
        /** Stub */
        setFilterChaining() { }

        /**
         * returns active filter expression for each data column
         *
         * @returns {string[]} active filter expressions
         */
        getFilters() {
            const self = this;

            return self.fields.map(
                (field) => self.filter[field] || null
            );
        }

        clearFilter() {
            this.filter = {};
        }

        sort(col, order) {
            const columnName = this.getFields()[col];
            switch (columnName) {
            case "itemAlias":
                this.sortBy = {
                    column: "alias",
                    order
                };
                break;
            default:
                this.sortBy = null;
            }

            this.refresh();
        }
    }

    module.DataGridItem = DataGridItem;
})();

/**
 * Dialog for editing the audit log viewer's filter.
 */
(function() {
    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-log-viewer-filter-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * Returns the widget configuration for button for one of the dialog's
     * user options.
     *
     * @param {string} buttonName Name of the button.
     * @param {string[]} [additionalCssClasses] Additional CSS classes to
     *  create the button with.
     * @returns {object}
     */
    function makeButtonConfig(buttonName, additionalCssClasses) {
        return {
            "ui": "iq-button",
            "config": {
                "class-name": ["iq-button", "iq-variant-01", ...(additionalCssClasses ?? [])].join(" "),
                "template": "default/iq-button.iq-variant-01",
                "label": `\${audit-log-viewer.filterDialog.buttons.${buttonName.toLocaleLowerCase()}}`,
                "_controllers_": getControllerConfig(`button${buttonName}`)
            }
        };
    }

    /**
     * Adds a new row to the filter table.
     *
     * @param {object} controllerState
     * @param {array} options Available options selectable via the select box.
     *  This parameter is passed through to the iq-select-box widget.
     * @param {object} [rowData] Object of the row's entry.
     */
    function addFilterRow(controllerState, options, rowData = {}) {
        const container = controllerState.getInstance("filterGrid"),
            selectJunctionMode = controllerState.getInstance("selectJunctionMode");

        controllerState.data.attributeRows.push(rowData);
        module.createAttributeRow(container, options, rowData, controllerState.data.attributeRows, () => {
            if (controllerState.data.attributeRows.length > 1) {
                selectJunctionMode.unlock();
            } else {
                selectJunctionMode.lock();
            }
        });
    }

    /**
     * Removes all rows from the filter table.
     *
     * @param {object} controllerState
     */
    function clearFilterRows(controllerState) {
        const container = controllerState.getInstance("filterGrid");

        container.controls.slice().forEach((child) => child.delete());
        controllerState.data.attributeRows = [];
    }

    /**
     * Sets the primary record type in the type selection and updates the
     * options available in the sub type selection.
     *
     * @param {object} controllerState
     * @param {string} type Record type to set as currently selected.
     */
    function setPrimaryType(controllerState, type) {
        const { getFilterTypeMap, collectFilterTypes } = module,
            selectSubType = controllerState.getInstance("selectSubType");

        const typeProperties = getFilterTypeMap()[type],
            options = [
                {
                    label: "${audit-log-viewer.filterDialog.allTypes}",
                    value: null
                }
            ];

        if (typeProperties) {
            options.push(...typeProperties.subFilter.map(({ displayName }, idx) => ({
                label: displayName,
                value: idx
            })));
        }

        selectSubType.setOptions(options);
        selectSubType.setValue(null);
        controllerState.data.primaryType = type;
        controllerState.data.subType = null;
        controllerState.data.currentAttributeOptions = [];
        controllerState.data.filters = collectFilterTypes(type);
    }

    /**
     * Sets the record sub type in the type selection.
     *
     * @param {object} controllerState
     * @param {string} type Record sub type to set as currently selected.
     */
    function setSubType(controllerState, type) {
        const { getFilterTypeMap, collectFilterTypes } = module,
            buttonAddFilterRow = controllerState.getInstance("buttonAddFilterRow"),
            currentAttributeOptions = controllerState.data.currentAttributeOptions = [];

        const subTypeProperties = getFilterTypeMap()[controllerState.data.primaryType]?.subFilter[type],
            options = [];

        if (type !== null && subTypeProperties) {
            currentAttributeOptions.push(...subTypeProperties.attributes.map(({ displayName, prop, type: valueType }) => ({
                label: displayName,
                value: prop,
                type: valueType
            })));
            options.push(...currentAttributeOptions);
        }

        if (options.length > 0) {
            buttonAddFilterRow.unlock();
        } else {
            buttonAddFilterRow.lock();
        }

        controllerState.data.subType = type;
        controllerState.data.filters = collectFilterTypes(controllerState.data.primaryType, type);
        clearFilterRows(controllerState);
    }

    const buttonHandlers = {
        onSelectStartDate: async function onSelectStartDate(controllerState) {
            const { requestDateAndTime, formatDateTime } = shmi.requires("visuals.tools.date"),
                btn = controllerState.getInstance("buttonSelectStartDate");

            const timestamp = await requestDateAndTime({ timestamp: controllerState.data.startTime });
            if (timestamp === null) {
                return;
            }

            controllerState.data.startTime = timestamp;
            btn.setLabel(formatDateTime(new Date(timestamp), {
                datestring: shmi.localize("${alarmlist_dateformat}")
            }));
        },
        onSelectEndDate: async function onSelectEndDate(controllerState) {
            const { requestDateAndTime, formatDateTime } = shmi.requires("visuals.tools.date"),
                btn = controllerState.getInstance("buttonSelectEndDate");

            const timestamp = await requestDateAndTime({ timestamp: controllerState.data.endTime });
            if (timestamp === null) {
                return;
            }

            controllerState.data.endTime = timestamp;
            btn.setLabel(formatDateTime(new Date(timestamp), {
                datestring: shmi.localize("${alarmlist_dateformat}")
            }));
        }
    };

    const selectHandlers = {
        onSelectPrimaryType: function onSelectPrimaryType(controllerState, slot, type, event) {
            setPrimaryType(controllerState, event.detail.value);
        },
        onSelectSubType: function onSelectSubType(controllerState, slot, type, event) {
            setSubType(controllerState, event.detail.value);
        }
    };

    /**
     * Creates a controller configuration for a new dialog.
     *
     * @param {function} resolve Function called upon user confirmation or
     *  cancellation.
     * @param {object} [filterData] Filter data to pre-populate the dialog
     *  with.
     * @returns {object}
     */
    function makeDialogController(resolve, filterData = {}) {
        const defaultStartTime = new Date,
            defaultEndTime = new Date;
        defaultStartTime.setHours(0, 0, 0, 0);
        defaultEndTime.setHours(23, 59, 59, 999);

        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonAddFilterRow: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonSelectStartDate: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonSelectEndDate: {
                    ui: "iq-button",
                    events: ["click"]
                },
                selectJunctionMode: {
                    ui: "iq-select-box",
                    events: ["change"]
                },
                selectSubType: {
                    ui: "iq-select-box",
                    events: ["change"]
                },
                selectType: {
                    ui: "iq-select-box",
                    events: ["change"]
                },
                filterGrid: {
                    ui: "container"
                }
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {
                const { formatDateTime } = shmi.requires("visuals.tools.date"),
                    { getFilterTypeMap } = module,
                    selectJunctionMode = state.getInstance("selectJunctionMode"),
                    selectType = state.getInstance("selectType"),
                    selectSubType = state.getInstance("selectSubType");

                state.getInstance("buttonSelectStartDate").setLabel(formatDateTime(new Date(state.data.startTime), {
                    datestring: shmi.localize("${alarmlist_dateformat}")
                }));

                state.getInstance("buttonSelectEndDate").setLabel(formatDateTime(new Date(state.data.endTime), {
                    datestring: shmi.localize("${alarmlist_dateformat}")
                }));

                selectJunctionMode.lock();
                state.getInstance("buttonAddFilterRow").lock();

                selectType.setOptions([
                    {
                        label: "${audit-log-viewer.filterDialog.allTypes}",
                        value: null
                    },
                    ...getFilterTypeMap().map(({ displayName }, idx) => ({
                        label: displayName,
                        value: idx
                    }))
                ]);

                selectJunctionMode.setValue(filterData.junctionMode ?? "AND");
                selectType.setValue(filterData.type ?? null);
                setPrimaryType(state, filterData.type ?? null);

                if (filterData.subType !== null) {
                    selectSubType.setValue(filterData.subType ?? null);
                    setSubType(state, filterData.subType ?? null);
                }

                filterData.attributeRows?.forEach((row) => addFilterRow(state, state.data.currentAttributeOptions, row));
            },
            onDisable: function(state) {},
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "selectJunctionMode":
                    state.data.junctionMode = event.detail.value;
                    break;
                case "selectType":
                    selectHandlers.onSelectPrimaryType(state, slot, type, event);
                    break;
                case "selectSubType":
                    selectHandlers.onSelectSubType(state, slot, type, event);
                    break;
                case "buttonAddFilterRow": {
                    module.createFilterAttributeSelectDialog(state.data.currentAttributeOptions).then((selected) => {
                        if (selected) {
                            addFilterRow(state, state.data.currentAttributeOptions, { attributeName: selected.value, comparisonType: "=" });
                        }
                    });
                    break;
                }
                case "buttonSelectStartDate":
                    buttonHandlers.onSelectStartDate(state);
                    break;
                case "buttonSelectEndDate":
                    buttonHandlers.onSelectEndDate(state);
                    break;
                case "buttonApply":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve({
                            startTime: state.data.startTime,
                            endTime: state.data.endTime,
                            junctionMode: state.data.junctionMode,
                            type: state.data.primaryType,
                            subType: state.data.subType,
                            filters: state.data.filters,
                            attributeRows: state.data.attributeRows.slice()
                        });
                        state.getInstance("dialog").hide();
                    }
                    break;
                case "buttonCancel":
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                startTime: filterData.startTime ?? defaultStartTime.getTime(),
                endTime: filterData.endTime ?? defaultEndTime.getTime(),
                junctionMode: filterData.junctionMode ?? "AND",
                currentAttributeOptions: [],
                resolved: false,
                primaryType: filterData.type ?? null,
                subType: filterData.subType ?? null,
                filters: filterData.filters ?? [],
                attributeRows: []
            }
        };
    }

    /**
     * Generates a widget tree configuration for the dialog to be passed to
     * `BaseControl.addControl()`.
     *
     * @param {function} resolve Function called upon user confirmation or
     *  cancellation.
     * @param {object} [filterData] Filter data to pre-populate the dialog
     *  with.
     * @returns {object}
     */
    function makeDialogLayout(resolve, filterData = {}) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "auditLogViewerFilterDialog",
                "class-name": "dialog-box option audit-log-viewer-filter-dialog",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${audit-log-viewer.filterDialog.title}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(resolve, filterData),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container grid-spacing main-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container grid-spacing",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": false
                            },
                            "children": [
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01 filterlabel",
                                        "template": "default/iq-text.iq-variant-01",
                                        "text": "${audit-log-viewer.filterDialog.labelTimespanFilter}"
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-02 date-time-selection",
                                        "template": "default/iq-button.date-time-selection",
                                        "label": "uninitialized",
                                        "_controllers_": getControllerConfig("buttonSelectStartDate")
                                    }
                                },
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01",
                                        "template": "default/iq-text.iq-variant-01",
                                        "name": "placeholder",
                                        "text": "-"
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-02 date-time-selection",
                                        "template": "default/iq-button.date-time-selection",
                                        "label": "uninitialized",
                                        "_controllers_": getControllerConfig("buttonSelectEndDate")
                                    }
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container filter-type-grid grid-spacing",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": false
                            },
                            "children": [
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01 filter-category-label",
                                        "template": "default/iq-text.iq-variant-01",
                                        "text": "${audit-log-viewer.filterDialog.labelFilterCategory}"
                                    }
                                },
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01 filter-type-label",
                                        "template": "default/iq-text.iq-variant-01",
                                        "text": "${audit-log-viewer.filterDialog.labelFilterType}"
                                    }
                                },
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": "iq-select-box iq-variant-01 type-select",
                                        "template": "default/iq-select-box.iq-variant-01",
                                        "name": "placeholder",
                                        "options": [
                                            {
                                                "label": "${audit-log-viewer.filterDialog.allTypes}",
                                                "value": null
                                            }
                                        ],
                                        "selected": 0,
                                        "label": null,
                                        "unit-text": null,
                                        "_controllers_": getControllerConfig("selectType")
                                    }
                                },
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": "iq-select-box iq-variant-01 sub-type-select",
                                        "template": "default/iq-select-box.iq-variant-01",
                                        "name": "placeholder",
                                        "options": [
                                            {
                                                "label": "${audit-log-viewer.filterDialog.allTypes}",
                                                "value": null
                                            }
                                        ],
                                        "selected": 0,
                                        "label": null,
                                        "unit-text": null,
                                        "_controllers_": getControllerConfig("selectSubType")
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-01 add-button",
                                        "template": "default/iq-button.iq-variant-01",
                                        "tooltip": "Add entry",
                                        "label": null,
                                        "show-icon": true,
                                        "show-text": false,
                                        "icon-src": "pics/system/controls/iq-user-controller/add-dark.svg",
                                        "inline-svg-icon": true,
                                        "_controllers_": getControllerConfig("buttonAddFilterRow")
                                    }
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container filter-grid-container",
                                "type": "iqflex",
                                "_controllers_": getControllerConfig("filterGrid")
                            },
                            "children": []
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container filter-grid-row add-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-secondary-align": "center",
                                "flex-wrap": false
                            },
                            "children": [
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": "iq-select-box iq-variant-01 junction-mode-select list-on-top",
                                        "template": "default/iq-select-box.iq-variant-01",
                                        "name": "placeholder",
                                        "options": [
                                            {
                                                "label": "AND",
                                                "value": "AND"
                                            },
                                            {
                                                "label": "OR",
                                                "value": "OR"
                                            }
                                        ],
                                        "selected": 0,
                                        "label": null,
                                        "unit-text": null,
                                        "_controllers_": getControllerConfig("selectJunctionMode")
                                    }
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container dialog-button-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer"
                            },
                            "children": [
                                makeButtonConfig("Cancel"),
                                makeButtonConfig("Apply", ["filled"])
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * Creates a dialog prompting the user for a filter configuration.
     *
     * @param {object} [filterData] Filter data to pre-populate the dialog
     *  with.
     * @returns {Promise} Promise that is resolved with the filter
     *  configuration on user confirmation or `null` on user cancellation.
     */
    async function createFilterDialog(filterData = {}) {
        const { displayLoadingOverlay } = shmi.requires("visuals.tools.loading-overlay"),
            customTypes = await displayLoadingOverlay("<<loading>>", shmi.visuals.session.ConnectSession.requestPromise("audit.list_custom_types", {}));

        module.setCustomTypes(customTypes);

        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(resolve, filterData);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                }
            });
        });
    }

    // Only export the "createFilterDialog" function if it hasn't already been
    // provided by a different layout module.
    if (!module.createFilterDialog) {
        module.createFilterDialog = createFilterDialog;
    }

    // MODULE CODE - END

    fLog("module loaded");
})();

/**
 * Type map and helper functions for the filter dialog.
 */
(function() {
    const MODULE_NAME = "visuals.controls.audit-log-viewer",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const FILTERS = [
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.alarms}",
            subFilter: [
                {
                    displayName: "Alarms",
                    type: {
                        type: "alarm",
                        subType: null
                    },
                    attributes: []
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.ioOps}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.writeDirect}",
                    type: {
                        type: "io",
                        subType: "writeDirect"
                    },
                    attributes: [
                        {
                            displayName: "itemAlias",
                            type: "item",
                            prop: "$.itemAlias"
                        },
                        {
                            displayName: "oldValue",
                            type: "auto",
                            prop: "$.oldValue"
                        },
                        {
                            displayName: "newValue",
                            type: "auto",
                            prop: "$.newValue"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.writeCmpxchg}",
                    type: {
                        type: "io",
                        subType: "writeCmpxchg"
                    },
                    attributes: [
                        {
                            displayName: "itemAlias",
                            type: "item",
                            prop: "$.itemAlias"
                        },
                        {
                            displayName: "oldValue",
                            type: "auto",
                            prop: "$.oldValue"
                        },
                        {
                            displayName: "newValue",
                            type: "auto",
                            prop: "$.newValue"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.methodCall}",
                    type: {
                        type: "io",
                        subType: "methodCall"
                    },
                    attributes: [
                        {
                            displayName: "methodAlias",
                            type: "string",
                            prop: "$.methodAlias"
                        }
                    ]
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.custom}",
            customTypeMarker: true,
            subFilter: []
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.loadProject}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.projectLoad}",
                    type: {
                        type: "project",
                        subType: "load"
                    },
                    attributes: [
                        {
                            displayName: "Project Name",
                            type: "string",
                            prop: "$.projectName"
                        },
                        {
                            displayName: "Path",
                            type: "string",
                            prop: "$.path"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.projectUnload}",
                    type: {
                        type: "project",
                        subType: "unload"
                    },
                    attributes: [
                        {
                            displayName: "Project Name",
                            type: "string",
                            prop: "$.projectName"
                        }
                    ]
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.recipes}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.recipeApply}",
                    type: {
                        type: "recipe",
                        subType: "apply"
                    },
                    attributes: [
                        {
                            displayName: "recipeVersionId",
                            type: "number",
                            prop: "$.recipeVersionId"
                        },
                        {
                            displayName: "recipeId",
                            type: "number",
                            prop: "$.recipeId"
                        },
                        {
                            displayName: "version",
                            type: "string",
                            prop: "$.version"
                        },
                        {
                            displayName: "recipeName",
                            type: "string",
                            prop: "$.recipeName"
                        },
                        {
                            displayName: "comment",
                            type: "string",
                            prop: "$.comment"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.recipeCapture}",
                    type: {
                        type: "recipe",
                        subType: "capture"
                    },
                    attributes: [
                        {
                            displayName: "recipeVersionId",
                            type: "number",
                            prop: "$.recipeVersionId"
                        },
                        {
                            displayName: "recipeId",
                            type: "number",
                            prop: "$.recipeId"
                        },
                        {
                            displayName: "version",
                            type: "string",
                            prop: "$.version"
                        },
                        {
                            displayName: "recipeName",
                            type: "string",
                            prop: "$.recipeName"
                        },
                        {
                            displayName: "comment",
                            type: "string",
                            prop: "$.comment"
                        }
                    ]
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.auth}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authLogin}",
                    type: {
                        type: "user",
                        subType: "authLogin"
                    },
                    attributes: []
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authLoginFailed}",
                    type: {
                        type: "user",
                        subType: "authLoginFailed"
                    },
                    attributes: []
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authLogout}",
                    type: {
                        type: "user",
                        subType: "authLogout"
                    },
                    attributes: []
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authToken}",
                    type: {
                        type: "user",
                        subType: "authToken"
                    },
                    attributes: []
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authTokenFailed}",
                    type: {
                        type: "user",
                        subType: "authTokenFailed"
                    },
                    attributes: []
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.disconnect}",
                    type: {
                        type: "user",
                        subType: "disconnect"
                    },
                    attributes: []
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authGetTokenForSelf}",
                    type: {
                        type: "user",
                        subType: "getToken"
                    },
                    attributes: [
                        {
                            displayName: "lifetime",
                            type: "number",
                            prop: "$.lifetime"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authGetTokenForUser}",
                    type: {
                        type: "user",
                        subType: "getTokenFor"
                    },
                    attributes: [
                        {
                            displayName: "lifetime",
                            type: "number",
                            prop: "$.lifetime"
                        },
                        {
                            displayName: "userId",
                            type: "string",
                            prop: "$.userId"
                        },
                        {
                            displayName: "realm",
                            type: "string",
                            prop: "$.realm"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authImpersonate}",
                    type: {
                        type: "user",
                        subType: "impersonate"
                    },
                    attributes: [
                        {
                            displayName: "sourceAuthRealm",
                            type: "string",
                            prop: "$.source.authRealm"
                        },
                        {
                            displayName: "sourceUserGuid",
                            type: "string",
                            prop: "$.source.userGuid"
                        },
                        {
                            displayName: "targetAuthRealm",
                            type: "string",
                            prop: "$.target.authRealm"
                        },
                        {
                            displayName: "targetUserGuid",
                            type: "string",
                            prop: "$.target.userGuid"
                        }
                    ]
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.userAdmin}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.accessgroupCreate}",
                    type: {
                        type: "accessgroup",
                        subType: "create"
                    },
                    attributes: [
                        {
                            displayName: "name",
                            type: "string",
                            prop: "$.createdObject.name"
                        },
                        {
                            displayName: "accessRights",
                            type: "array",
                            prop: "$.createdObject.accessRights"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.createdObject.description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.accessgroupDelete}",
                    type: {
                        type: "accessgroup",
                        subType: "delete"
                    },
                    attributes: [
                        {
                            displayName: "name",
                            type: "string",
                            prop: "$.deletedObjects[0].name"
                        },
                        {
                            displayName: "accessRights",
                            type: "array",
                            prop: "$.deletedObjects[0].accessRights"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.deletedObjects[0].description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.accessgroupModify}",
                    type: {
                        type: "accessgroup",
                        subType: "modify"
                    },
                    attributes: [
                        {
                            displayName: "name",
                            type: "string",
                            prop: "$.oldObject.name"
                        },
                        {
                            displayName: "accessRights",
                            type: "array",
                            prop: "$.oldObject.accessRights"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.oldObject.description"
                        },
                        {
                            displayName: "name",
                            type: "string",
                            prop: "$.newObject.name"
                        },
                        {
                            displayName: "accessRights",
                            type: "array",
                            prop: "$.newObject.accessRights"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.newObject.description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.userCreate}",
                    type: {
                        type: "user",
                        subType: "create"
                    },
                    attributes: [
                        {
                            displayName: "realm",
                            type: "string",
                            prop: "$.createdObject.realm"
                        },
                        {
                            displayName: "username",
                            type: "string",
                            prop: "$.createdObject.username"
                        },
                        {
                            displayName: "firstName",
                            type: "string",
                            prop: "$.createdObject.firstName"
                        },
                        {
                            displayName: "middleName",
                            type: "string",
                            prop: "$.createdObject.middleName"
                        },
                        {
                            displayName: "lastName",
                            type: "string",
                            prop: "$.createdObject.lastName"
                        },
                        {
                            displayName: "locale",
                            type: "string",
                            prop: "$.createdObject.locale"
                        },
                        {
                            displayName: "passwordExpiryDuration",
                            type: "number",
                            prop: "$.createdObject.passwordExpiryDuration"
                        },
                        {
                            displayName: "passwordForceExpired",
                            type: "boolean",
                            prop: "$.createdObject.passwordForceExpired"
                        },
                        {
                            displayName: "autoLogoutDuration",
                            type: "number",
                            prop: "$.createdObject.autoLogoutDuration"
                        },
                        {
                            displayName: "disabled",
                            type: "boolean",
                            prop: "$.createdObject.disabled"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.createdObject.description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.userDelete}",
                    type: {
                        type: "user",
                        subType: "delete"
                    },
                    attributes: [
                        {
                            displayName: "realm",
                            type: "string",
                            prop: "$.deletedObjects[0].realm"
                        },
                        {
                            displayName: "username",
                            type: "string",
                            prop: "$.deletedObjects[0].username"
                        },
                        {
                            displayName: "firstName",
                            type: "string",
                            prop: "$.deletedObjects[0].firstName"
                        },
                        {
                            displayName: "middleName",
                            type: "string",
                            prop: "$.deletedObjects[0].middleName"
                        },
                        {
                            displayName: "lastName",
                            type: "string",
                            prop: "$.deletedObjects[0].lastName"
                        },
                        {
                            displayName: "locale",
                            type: "string",
                            prop: "$.deletedObjects[0].locale"
                        },
                        {
                            displayName: "disabled",
                            type: "boolean",
                            prop: "$.deletedObjects[0].disabled"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.deletedObjects[0].description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.userModify}",
                    type: {
                        type: "user",
                        subType: "modify"
                    },
                    attributes: [
                        {
                            displayName: "oldRealm",
                            type: "string",
                            prop: "$.oldObject.realm"
                        },
                        {
                            displayName: "oldUsername",
                            type: "string",
                            prop: "$.oldObject.username"
                        },
                        {
                            displayName: "oldFirstName",
                            type: "string",
                            prop: "$.oldObject.firstName"
                        },
                        {
                            displayName: "oldMiddleName",
                            type: "string",
                            prop: "$.oldObject.middleName"
                        },
                        {
                            displayName: "oldLastName",
                            type: "string",
                            prop: "$.oldObject.lastName"
                        },
                        {
                            displayName: "oldLocale",
                            type: "string",
                            prop: "$.oldObject.locale"
                        },
                        {
                            displayName: "oldDisabled",
                            type: "boolean",
                            prop: "$.oldObject.disabled"
                        },
                        {
                            displayName: "oldDescription",
                            type: "string",
                            prop: "$.oldObject.description"
                        },
                        {
                            displayName: "newRealm",
                            type: "string",
                            prop: "$.newObject.realm"
                        },
                        {
                            displayName: "newUsername",
                            type: "string",
                            prop: "$.newObject.username"
                        },
                        {
                            displayName: "newFirstName",
                            type: "string",
                            prop: "$.newObject.firstName"
                        },
                        {
                            displayName: "newMiddleName",
                            type: "string",
                            prop: "$.newObject.middleName"
                        },
                        {
                            displayName: "newLastName",
                            type: "string",
                            prop: "$.newObject.lastName"
                        },
                        {
                            displayName: "newLocale",
                            type: "string",
                            prop: "$.newObject.locale"
                        },
                        {
                            displayName: "newDisabled",
                            type: "boolean",
                            prop: "$.newObject.disabled"
                        },
                        {
                            displayName: "newDescription",
                            type: "string",
                            prop: "$.newObject.description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.usergroupCreate}",
                    type: {
                        type: "usergroup",
                        subType: "create"
                    },
                    attributes: [
                        {
                            displayName: "realm",
                            type: "string",
                            prop: "$.createdObject.realm"
                        },
                        {
                            displayName: "groupname",
                            type: "string",
                            prop: "$.createdObject.groupname"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.createdObject.description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.usergroupDelete}",
                    type: {
                        type: "usergroup",
                        subType: "delete"
                    },
                    attributes: [
                        {
                            displayName: "realm",
                            type: "string",
                            prop: "$.deletedObjects[0].realm"
                        },
                        {
                            displayName: "groupname",
                            type: "string",
                            prop: "$.deletedObjects[0].groupname"
                        },
                        {
                            displayName: "description",
                            type: "string",
                            prop: "$.deletedObjects[0].description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.usergroupModify}",
                    type: {
                        type: "usergroup",
                        subType: "delete"
                    },
                    attributes: [
                        {
                            displayName: "oldRealm",
                            type: "string",
                            prop: "$.oldObject.realm"
                        },
                        {
                            displayName: "oldGroupname",
                            type: "string",
                            prop: "$.oldObject.groupname"
                        },
                        {
                            displayName: "oldDescription",
                            type: "string",
                            prop: "$.oldObject.description"
                        },
                        {
                            displayName: "newRealm",
                            type: "string",
                            prop: "$.newObject.realm"
                        },
                        {
                            displayName: "newGroupname",
                            type: "string",
                            prop: "$.newObject.groupname"
                        },
                        {
                            displayName: "newDescription",
                            type: "string",
                            prop: "$.newObject.description"
                        }
                    ]
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.userSelfModify}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.userChangeLanguage}",
                    type: {
                        type: "user",
                        subType: "changeLanguage"
                    },
                    attributes: [
                        {
                            displayName: "oldLanguage",
                            type: "string",
                            prop: "$.oldLanguage"
                        },
                        {
                            displayName: "newLanguage",
                            type: "string",
                            prop: "$.newLangauge"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.userChangeSecret}",
                    type: {
                        type: "user",
                        subType: "changeSecret"
                    },
                    attributes: []
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.ioStatus}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.ioExtensionStatusChange}",
                    type: {
                        type: "ioExtension",
                        subType: "statusChange"
                    },
                    attributes: [
                        {
                            displayName: "extensionName",
                            type: "string",
                            prop: "$.extensionName"
                        },
                        {
                            displayName: "oldState",
                            type: "string",
                            prop: "$.oldState"
                        },
                        {
                            displayName: "newState",
                            type: "string",
                            prop: "$.newState"
                        }
                    ]
                }
            ]
        },
        {
            displayName: "${audit-log-viewer.filterDialog.categoryLabels.authStatus}",
            subFilter: [
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authRealmLoadFailed}",
                    type: {
                        type: "authRealm",
                        subType: "loadFailed"
                    },
                    attributes: [
                        {
                            displayName: "name",
                            type: "string",
                            prop: "$.name"
                        },
                        {
                            displayName: "internalName",
                            type: "string",
                            prop: "$.internalName"
                        },
                        {
                            displayName: "version",
                            type: "string",
                            prop: "$.version"
                        },
                        {
                            displayName: "decription",
                            type: "string",
                            prop: "$.description"
                        },
                        {
                            displayName: "unlocalizedErrorMessage",
                            type: "string",
                            prop: "$.unlocalizedErrorMessage"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authRealmLoadSucceeded}",
                    type: {
                        type: "authRealm",
                        subType: "loadSuccess"
                    },
                    attributes: [
                        {
                            displayName: "name",
                            type: "string",
                            prop: "$.name"
                        },
                        {
                            displayName: "internalName",
                            type: "string",
                            prop: "$.internalName"
                        },
                        {
                            displayName: "version",
                            type: "string",
                            prop: "$.version"
                        },
                        {
                            displayName: "decription",
                            type: "string",
                            prop: "$.description"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authRealmStatusDisconnected}",
                    type: {
                        type: "authRealm",
                        subType: "statusDisconnected"
                    },
                    attributes: [
                        {
                            displayName: "authRealmName",
                            type: "string",
                            prop: "$.authRealmName"
                        },
                        {
                            displayName: "oldState",
                            type: "string",
                            prop: "$.oldState"
                        },
                        {
                            displayName: "newState",
                            type: "string",
                            prop: "$.newState"
                        }
                    ]
                },
                {
                    displayName: "${audit-log-viewer.filterDialog.typeLabels.authRealmStatusConnected}",
                    type: {
                        type: "authRealm",
                        subType: "statusConnected"
                    },
                    attributes: [
                        {
                            displayName: "authRealmName",
                            type: "string",
                            prop: "$.authRealmName"
                        },
                        {
                            displayName: "oldState",
                            type: "string",
                            prop: "$.oldState"
                        },
                        {
                            displayName: "newState",
                            type: "string",
                            prop: "$.newState"
                        }
                    ]
                }
            ]
        }
    ];

    /**
     * Returns type filter information.
     */
    module.getFilterTypeMap = function getFilterTypeMap() {
        return FILTERS;
    };

    /**
     * Sets the custom types in the type filter map.
     *
     * @param {string[]} customTypeIds
     */
    module.setCustomTypes = function setCustomTypes(customTypeIds) {
        const customCategory = FILTERS.find(({ customTypeMarker }) => customTypeMarker);
        if (!customCategory) {
            return;
        }

        customCategory.subFilter = customTypeIds.map((subType) => ({
            displayName: `\${audit-log-viewer.recordTitle.custom.${subType.type_id}}`,
            type: {
                type: "custom",
                subType: subType.type_id
            },
            attributes: []
        }));
    };

    /**
     * Returns the types selected by the given primary and secondary filter category.
     *
     * @param {number} primaryIdx
     * @param {?number} [secondaryIdx]
     * @returns {array}
     */
    module.collectFilterTypes = function collectFilterTypes(primaryIdx, secondaryIdx = null) {
        const primaryTypeInfo = FILTERS[primaryIdx];
        if (!primaryTypeInfo) {
            return [];
        } else if (secondaryIdx === null) {
            return primaryTypeInfo.subFilter.map(({ type }) => type);
        }

        const secondaryTypeInfo = primaryTypeInfo.subFilter[secondaryIdx];
        if (!secondaryTypeInfo) {
            return [];
        }

        return [ secondaryTypeInfo.type ];
    };
})();

/**
 * audit-log-viewer widget
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-list",
 *     "template": "default/audit-log-viewer"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "AuditLogViewer", // control name in camel-case
        uiType = "audit-log-viewer", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": `${uiType}`,
        "name": null,
        "template": "default/audit-log-viewer.iq-variant-01",
        "label": uiType,
        "showRawPayload": false,
        "columnConfig": {
            "col1": {
                "fields": ["severityIcon"],
                "column-width": "60px"
            },
            "col2": {
                "fields": ["timestampIn"],
                "column-width": "150px"
            },
            "col3": {
                "fields": ["timestampOut"],
                "column-width": "150px",
                "hidden": true
            },
            "col4": {
                "fields": ["type"],
                "column-width": "100px",
                "hidden": true
            },
            "col5": {
                "fields": ["subType"],
                "column-width": "150px",
                "hidden": true
            },
            "col6": {
                "fields": ["recordTitle"]
            },
            "col7": {
                "fields": ["recordId"],
                "column-width": "300px",
                "hidden": true
            }
        }
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    // declare private functions - START

    function getRandomId() {
        return Math.random().toString(36).substring(2, 11);
    }

    function getDetailsTextForRecord(self, record) {
        if (record.type === "alarm") {
            return [
                shmi.evalString(shmi.localize(`\${alarm_title_${record.data.index}}`), record.data),
                shmi.evalString(shmi.localize(`\${alarm_msg_${record.data.index}}`), record.data)
            ].join("\n");
        } else {
            const localeVariable = record.sub_type?.length ? `\${audit-log-viewer.recordDetail.${record.type}.${record.sub_type}}` : `\${audit-log-viewer.recordDetail.${record.type}}`;

            return shmi.evalString(
                shmi.localize(localeVariable),
                {
                    payload: record,
                    userCache: self.vars.dataGrid.getGuidMap()
                }
            );
        }
    }

    /**
     * @param {HTMLElement | undefined | null} element
     * @param {string} text
     */
    function setElementText(element, text) {
        if (element) {
            element.textContent = text;
        }
    }

    /**
     * Reads the contents of an element and treats it as a localization
     * variable. The localized text is written back to the element.
     *
     * @param {HTMLElement | undefined | null} element
     */
    function translateElement(element) {
        if (!element) {
            return;
        }

        element.textContent = shmi.localize(element.textContent);
    }

    /**
     * Clears the data display that shows the currently selected entry.
     *
     * @param {*} self
     */
    function clearCurrentData(self) {
        setElementText(self.vars.entryContentElement, "");
        setElementText(self.vars.rawContentElement, "");

        Object.values(self.vars.metaElements).forEach((element) => element.textContent = "");
    }

    /**
     * Sets the data displayed as the currently selected entry.
     *
     * @param {*} self
     * @param {object} data
     */
    function setCurrentData(self, data) {
        const { formatDateTime } = shmi.requires("visuals.tools.date");
        let username = null;

        setElementText(self.vars.metaElements["record-type"], `${data.type} ${data.sub_type}`);
        setElementText(self.vars.metaElements["remote-endpoint"], data.remote_endpoint);
        setElementText(self.vars.metaElements["record-id"], data.record_id);
        setElementText(self.vars.metaElements["severity"], data.severity);

        if (data.user_id === null) {
            setElementText(self.vars.metaElements["user-id"], shmi.localize("${audit-log-viewer.userSystemEvent}"));
        } else {
            username = self.vars.dataGrid.getUsername(data.user_realm, data.user_id);
            if (username) {
                setElementText(self.vars.metaElements["user-id"], `$<${data.user_realm}>:${username}`);
            } else {
                setElementText(self.vars.metaElements["user-id"], `$<${data.user_realm}>:${data.user_id}`);
            }
        }

        setElementText(self.vars.metaElements["timestamp-in"], formatDateTime(new Date(data.timestamp_in / 10000), {
            datestring: shmi.localize("${alarmlist_dateformat}")
        }));

        if (data.timestamp_out === data.timestamp_in) {
            setElementText(self.vars.metaElements["timestamp-out"], "-");
        } else if (data.timestamp_out === null) {
            setElementText(self.vars.metaElements["timestamp-out"], shmi.localize("${audit-log-viewer.eventOngoing}"));
        } else {
            setElementText(self.vars.metaElements["timestamp-out"], formatDateTime(new Date(data.timestamp_out / 10000), {
                datestring: shmi.localize("${alarmlist_dateformat}")
            }));
        }

        setElementText(self.vars.entryContentElement, getDetailsTextForRecord(self, data));
        if (self.config.showRawPayload && data.data) {
            setElementText(self.vars.rawContentElement, JSON.stringify(data.data, null, 4));
        } else {
            setElementText(self.vars.rawContentElement, "");
        }
    }

    function setupListeners(self) {
        const { createFilterDialog } = shmi.requires("visuals.controls.audit-log-viewer"),
            { controls, listeners } = self.vars;

        if (controls.btnRefresh) {
            listeners.push(
                controls.btnRefresh.listen("click", () => {
                    self.vars.dataGrid.refresh();
                })
            );
        }

        if (controls.btnFilter) {
            listeners.push(
                controls.btnFilter.listen("click", async () => {
                    const filterSettings = await createFilterDialog(self.vars.lastFilter);
                    if (filterSettings) {
                        clearCurrentData(self);
                        self.vars.lastFilter = filterSettings;
                        self.vars.dataGrid.setFilterEx(filterSettings);
                        self.vars.dataGrid.refresh();
                        controls.btnClearFilter?.element.classList.remove("hidden");
                    }
                })
            );
        }

        if (controls.btnClearFilter) {
            listeners.push(
                controls.btnClearFilter.listen("click", () => {
                    controls.btnClearFilter.element.classList.add("hidden");
                    self.vars.dataGrid.clearFilter();
                    self.vars.dataGrid.refresh();
                })
            );
        }

        listeners.push(controls.ct.listen("select", (event) => {
            if ((event.detail.type === 1 || event.detail.type === 2) && event.detail.selRowIndex.length === 1) {
                logger.log("Selection:", self.vars.dataGrid.getRowDataRaw(event.detail.selRows[0]));
                setCurrentData(self, self.vars.dataGrid.getRowDataRaw(event.detail.selRows[0]));
            } else {
                logger.log("Nothing selected");
                clearCurrentData(self);
            }
        }));
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            controls: {
                ct: null,
                btnRefresh: null,
                btnFilter: null
            },
            metaElements: {},
            entryContentElement: null,
            rawContentElement: null,
            dataGridId: null,
            dataGrid: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const { makeAuditLogListConfig, DataGridAuditLog } = shmi.requires("visuals.controls.audit-log-viewer"),
                    buttonsAnchorRefresh = shmi.getUiElement("button-anchor-refresh", this.element),
                    buttonsAnchorFilter = shmi.getUiElement("button-anchor-filter", this.element),
                    buttonsAnchorClearFilter = shmi.getUiElement("button-anchor-clear-filter", this.element),
                    tableAnchor = shmi.getUiElement("table-anchor", this.element),
                    entryMetaContainer = shmi.getUiElement("entry-meta-container", this.element);

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                const datagrid = new DataGridAuditLog(this.vars.dataGridId, {
                    query: (params) => shmi.visuals.session.ConnectSession.requestPromise("audit.query", params),
                    getUserCache: () => shmi.visuals.session.ConnectSession.requestPromise("audit.get_user_cache", {})
                });
                this.imports.dgm.grids[this.vars.dataGridId] = datagrid;
                this.vars.dataGrid = datagrid;

                if (buttonsAnchorRefresh) {
                    // Workaround for shmi.getControlByElement called inside
                    // shmi.createControl throwing an exception if `data-ui`
                    // is set to something that's not the type of a widget.
                    buttonsAnchorRefresh.removeAttribute("data-ui");
                    this.vars.controls.btnRefresh = shmi.createControl("iq-button", buttonsAnchorRefresh, {
                        "class-name": "iq-button iq-variant-01 outlined",
                        "label": "${audit-log-viewer.buttons.refresh}",
                        "icon-src": "pics/system/controls/audit-log-viewer/refresh-icon.svg",
                        "inline-svg-icon": true,
                        "show-icon": true
                    }, null, "from");
                }

                if (buttonsAnchorFilter) {
                    // Workaround for shmi.getControlByElement called inside
                    // shmi.createControl throwing an exception if `data-ui`
                    // is set to something that's not the type of a widget.
                    buttonsAnchorFilter.removeAttribute("data-ui");
                    this.vars.controls.btnFilter = shmi.createControl("iq-button", buttonsAnchorFilter, {
                        "class-name": "iq-button iq-variant-01 outlined",
                        "label": "${alarmlist_set_timefilter}",
                        "icon-src": "pics/system/controls/audit-log-viewer/filter-icon.svg",
                        "inline-svg-icon": true,
                        "show-icon": true
                    }, null, "from");
                }

                if (buttonsAnchorClearFilter) {
                    // Workaround for shmi.getControlByElement called inside
                    // shmi.createControl throwing an exception if `data-ui`
                    // is set to something that's not the type of a widget.
                    buttonsAnchorClearFilter.removeAttribute("data-ui");
                    this.vars.controls.btnClearFilter = shmi.createControl("iq-button", buttonsAnchorClearFilter, {
                        "class-name": "iq-button iq-variant-01 outlined hidden",
                        "label": "${audit-log-viewer.buttons.clearFilter}",
                        "icon-src": "pics/system/controls/audit-log-viewer/clear-filter-icon.svg",
                        "inline-svg-icon": true,
                        "show-icon": true
                    }, null, "from");
                }

                if (tableAnchor) {
                    // Workaround for shmi.getControlByElement called inside
                    // shmi.createControl throwing an exception if `data-ui`
                    // is set to something that's not the type of a widget.
                    tableAnchor.removeAttribute("data-ui");
                    this.vars.controls.ct = shmi.createControl("complex-table2", tableAnchor, makeAuditLogListConfig(datagrid, this.config.columnConfig ?? defConfig.columnConfig), null, "from");
                }

                // Check if any of the widgets could not be created.
                const children = Object.values(this.vars.controls);
                if (children.some((child) => child === null)) {
                    children.forEach((child) => child?.delete(true));
                    this.element.textContent = "Unable to create some child widgets for audit log. This control will not work.";
                    logger.error("Unable to create child widgets for audit log:", Object.entries(this.vars.controls).filter(([, child]) => !child).map(([key]) => key));
                    return;
                }

                this.vars.entryContentElement = shmi.getUiElement("entry-content", this.element);
                this.vars.rawContentElement = shmi.getUiElement("entry-raw", this.element);

                // Process the metadata display by localizing all label
                // elements and collecting the value ones.
                Array.from(entryMetaContainer.children).forEach((child) => {
                    if (!child.classList.contains("entry-meta")) {
                        // We're not interested in a non-entry-meta node.
                    } else if (child.classList.contains("label")) {
                        // The node is a label. Translate it!
                        translateElement(child);
                    } else if (child.classList.contains("value")) {
                        // The node is a value. Store it!
                        const dataUi = child.getAttribute("data-ui");
                        if (dataUi) {
                            this.vars.metaElements[dataUi] = child;
                        }
                    }
                });
            },
            /* called when control is enabled */
            onEnable: function onEnable() {
                const { controls } = this.vars;

                setupListeners(this);

                Object.values(controls).forEach((ctrl) => ctrl?.enable());
            },
            onLock: function onLock() {
                this.vars.listeners.forEach((sub) => sub.unlisten());
                this.vars.listeners = [];
                Object.values(this.vars.controls).forEach((ctrl) => ctrl?.lock());
            },
            onUnlock: function onUnlock() {
                const { controls } = this.vars;

                setupListeners(this);

                Object.values(controls).forEach((ctrl) => ctrl?.unlock());
            },
            /* called when control is disabled */
            onDisable: function onDisable() {
                this.vars.listeners.forEach((sub) => sub.unlisten());
                this.vars.listeners = [];
                Object.values(this.vars.controls).forEach((ctrl) => ctrl?.disable());
            },
            onDelete: function onDelete() {
                if (this.vars.dataGridId) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
            },
            /**
             * @returns {?string} Username of the currently selected user or
             *  `null` if no user is selected.
             */
            getCurrentUser: function getCurrentUser() {
                return this.vars.currentUser;
            },
            refresh: function refresh() {
                this.vars.dataGrid?.refresh();
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

(function() {
    shmi.pkg("visuals.controls");

    var DIRECTION_UP = -1,
        DIRECTION_DOWN = 1;

    /////////////////////////////////////////

    /**
     * Checks whether the combination of lhs and rhs is in `knownObjects`.
     *
     * @param {*} lhs
     * @param {*} rhs
     * @param {{lhs: any, rhs: any}[]} knownObjects List of known combinations
     *  of lhs and rhs.
     * @returns {boolean} `true` if { lhs, rhs } is in `knownObjects`, `false`
     *  else.
     */
    function isKnown(lhs, rhs, knownObjects) {
        return knownObjects.findIndex((entry) => lhs === entry.lhs && rhs === entry.rhs) !== -1;
    }

    /**
     * Checks if two objects have the same keys.
     *
     * @param {object} lhs
     * @param {object} rhs
     * @returns {boolean} `true` if both objects have the same keys, `false`
     *  else.
     */
    function objectsHaveSameKeys(lhs, rhs) {
        const lhsKeys = Object.keys(lhs),
            rhsKeys = Object.keys(rhs);

        if (lhsKeys.length !== rhsKeys.length) {
            return false;
        }

        return lhsKeys.every((value) => rhsKeys.includes(value));
    }

    /**
     * Checks if the two values are equal, checking arrays and objects as well.
     *
     * Note that "equal" does not mean "the same". Objects and arrays having
     * equal contents are considered equal themselves. The values do not need
     * to reference the same object.
     *
     * @param {*} lhs Left-hand side value for the comparison.
     * @param {*} rhs Right-hand side value for the comparison.
     * @param {{lhs: any, rhs: any}[]} knownObjects List of known combinations
     *  of lhs and rhs.
     * @returns {boolean} `true` if lhs and rhs are considered equal, `false` else.
     */
    function isEqualDeepImpl(lhs, rhs, knownObjects = []) {
        if (lhs === rhs) {
            return true;
        } else if (typeof lhs !== typeof rhs || typeof lhs !== "object") {
            return false;
        } else if (Array.isArray(lhs) !== Array.isArray(rhs)) {
            return false;
        } else if (isKnown(lhs, rhs, knownObjects)) {
            return true;
        } else if (Array.isArray(lhs)) {
            // Arrays

            // Arrays need to have the same length to be considered equal.
            if (lhs.length !== rhs.length) {
                return false;
            }

            knownObjects.push({ lhs, rhs });
            return lhs.every((val, idx) => isEqualDeepImpl(val, rhs[idx], knownObjects));
        } else {
            // Objects
            const keys = Object.keys(lhs);

            // If the objects have different keys then they can't be equal.
            if (!objectsHaveSameKeys(lhs, rhs)) {
                return false;
            }

            knownObjects.push({ lhs, rhs });
            return keys.every((key) => isEqualDeepImpl(lhs[key], rhs[key], knownObjects));
        }
    }

    /**
     * Checks if the two values are equal, checking arrays and objects as well.
     *
     * Note that "equal" does not mean "the same". Objects and arrays having
     * equal contents are considered equal themselves. The values do not need
     * to reference the same object.
     *
     * @param {*} lhs Left-hand side value for the comparison.
     * @param {*} rhs Right-hand side value for the comparison.
     * @returns {boolean} `true` if lhs and rhs are considered equal, `false` else.
     */
    function isEqualDeep(lhs, rhs) {
        return isEqualDeepImpl(lhs, rhs, []);
    }

    /**
     * Checks if two "selection" objects are equal.
     *
     * @param {*} lhs
     * @param {*} rhs
     * @returns {boolean}
     */
    function isEqualSelection(lhs, rhs) {
        const lhsAug = Object.assign({}, lhs),
            rhsAug = Object.assign({}, rhs);

        delete lhsAug.dblClicked;
        delete rhsAug.dblClicked;

        return isEqualDeep(lhsAug, rhsAug);
    }

    /////////////////////////////////////////

    /**
     * getRowOffset - get offset of row within datagrid data
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID
     * @return {number} row offset or `null` if row ID not found
     */
    function getRowOffset(self, rowId) {
        let offset = null;

        const dgm = shmi.requires("visuals.session.DataGridManager"),
            g = dgm.getGrid(self.cConfig.table),
            subId = self.dgSubscription.id;

        if (rowId >= 0) {
            const gridOffset = self.pageBuf.bufferOffset,
                currentIds = g.getCurrentIDs(subId);
            const rowIndex = currentIds.findIndex((id) => id === rowId);

            if (rowIndex >= 0) {
                offset = gridOffset + rowIndex;
            }
        }

        return offset;
    }

    /**
     * setLastClicked - set info on last clicked row and its offset
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID
     */
    function setLastClicked(self, rowId) {
        if (rowId >= 0) {
            const rowOffset = getRowOffset(self, rowId);
            if (rowOffset !== null) {
                self.lastClicked.rowId = rowId;
                self.lastClicked.offset = rowOffset;
                return;
            }
        }

        self.lastClicked.rowId = -1;
        self.lastClicked.offset = 0;
    }

    /**
     * loadRange - load row data range
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID of last clicked row
     * @param {number} start data start offset
     * @param {number} length number of rows to fetch
     * @param {function} callback function called on completion
     */
    function loadRange(self, rowId, start, length, callback) {
        startLoading(self);
        const dgm = shmi.requires("visuals.session.DataGridManager"),
            sub = dgm.subscribePage(self.cConfig.table, start, length, (dgInfo) => {
                const currentIds = dgm.getCurrentIDs(self.cConfig.table, sub.id);
                currentIds.forEach((id) => {
                    self.addRowIdToSelection(id);
                });

                dgm.unsubscribe(self.cConfig.table, sub.id);
                setLastClicked(self, rowId);
                stopLoading(self);

                self.updateSelTypeFromSelRows();
                self.updateSelectionView();
                clearTimeout(self.sel_fire_to);
                const selectionData = shmi.cloneObject(self.selection);
                selectionData.dblClicked = false;
                self.sel_fire_to = setTimeout(function() {
                    self.fireSelChanged(selectionData);
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
                callback();
            });
    }

    /**
     * addSelectionRange - add range from last clicked row to row ID to current selection
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID
     * @return {promise} promise resolving when selection range has been added
     */
    function addSelectionRange(self, rowId) {
        return new Promise((resolve, reject) => {
            const rowOffset = getRowOffset(self, rowId);
            if (rowOffset === null) {
                reject(new Error("Could not find row offset"));
            } else {
                let start, end;
                if (rowOffset >= self.lastClicked.offset) {
                    end = rowOffset;
                    start = self.lastClicked.offset;
                } else {
                    end = self.lastClicked.offset;
                    start = rowOffset;
                }

                const length = (end - start) + 1;
                loadRange(self, rowId, start, length, resolve);
            }
        });
    }

    /**
     * createTableRow - create individual table row
     *
     * @param {ComplexTable2} self table control instance reference
     * @param {number} rowIdx buffer row index
     * @param {HTMLElement} tbodyElem table body element to generate row into
     * @param {boolean} isFirst true when row is first row of tbody
     *
     * @returns {undefined}
     */
    function createTableRow(self, rowIdx, tbodyElem, isFirst) {
        var nofCols = self.cConfig.resp["nof-cols"],
            trElem = null,
            tdElem = null,
            isMultiSelectMode = (self.cConfig.resp["select-mode"] === "MULTI"),
            isSingleSelectMode = (self.cConfig.resp["select-mode"] === "SINGLE"),
            isItemSelectMode = (self.cConfig.resp["select-mode"] === "ITEM"),
            tabCellElem = null,
            bufferRow = self.pageBuf.rows[rowIdx];

        if (bufferRow) {
            trElem = document.createElement("tr");
            if (bufferRow.rowId === null) {
                trElem.style.visibility = "hidden";
                bufferRow.visible = false;
            }
            tbodyElem.appendChild(trElem);
            bufferRow.trElem = trElem;
            if (((self.pageBuf.offset + rowIdx) % 2) === 0) {
                shmi.addClass(trElem, self.uiCssCl.evenRow);
            }
            if (isMultiSelectMode) {
                if (self.config["show-select-boxes"]) {
                    tdElem = document.createElement("td");
                    tabCellElem = document.createElement("div");
                    bufferRow.selCB = self.createSelCBox(tabCellElem, rowIdx, false, self, self.config["cb-checked-icon-src"], self.config["cb-undefined-icon-src"]);
                    bufferRow.selCB.setChangeCallback(function(pageBufRow, state) {
                        if (!self.locked) {
                            self.selectCBoxValChanged(pageBufRow, state, false, false, true, false);
                        }
                    });
                    tdElem.appendChild(tabCellElem);
                    trElem.appendChild(tdElem);
                }

                bufferRow.trSelCtrl = self.createSelRowCtrl(trElem, rowIdx, self);
                bufferRow.trSelCtrl.setChangeCallback(function(pageBufRow, state, shiftPressed, ctrlPressed, dblClicked) {
                    if (!self.locked) {
                        self.selectCBoxValChanged(pageBufRow, state, shiftPressed, ctrlPressed, false, dblClicked);
                    }
                });
            } else if (isSingleSelectMode || isItemSelectMode) {
                bufferRow.trSelCtrl = self.createSelRowCtrl(trElem, rowIdx, self);
                bufferRow.trSelCtrl.setChangeCallback(function(pageBufRow, state, shiftPressed, ctrlPressed, dblClicked) {
                    if (!self.locked) {
                        self.selectRowValChanged(pageBufRow, state, shiftPressed, ctrlPressed, dblClicked);
                    }
                });
            }
            for (var col = 0; col < nofCols; col++) {
                createTableCell(self, rowIdx, col, trElem, isFirst);
            }
        } else {
            console.error("[ComplexTable2]", "missing row descriptor for row ID:", rowIdx);
        }
    }

    /**
     * createTableCell - create individual table cell
     *
     * @param {ComplexTable2} self table control instance reference
     * @param {number} rowIdx buffer row index
     * @param {number} columnIdx table column index
     * @param {HTMLElement} trElem table row element to generate cell into
     * @param {boolean} isFirst true if is first row of tbody
     *
     * @returns {undefined}
     */
    function createTableCell(self, rowIdx, columnIdx, trElem, isFirst) {
        var columnConfig = self.cConfig.resp["column-org"][columnIdx],
            tabCellElem = document.createElement("div"),
            tdElem = document.createElement("td"),
            nofCtrls = columnConfig.fields.length;
        tdElem.appendChild(tabCellElem);
        trElem.appendChild(tdElem);
        shmi.addClass(tabCellElem, self.uiCssCl.tdCont);
        if (isFirst) {
            if (columnConfig.fields[0]["min-width"]) {
                tabCellElem.style.minWidth = columnConfig.fields[0]["min-width"];
            }
        }
        for (var ctrlIdx = 0; ctrlIdx < nofCtrls; ctrlIdx++) {
            createCellControl(self, rowIdx, columnIdx, ctrlIdx, columnConfig, tabCellElem);
        }
    }

    /**
     * createCellControl - create control configured for specific table cell
     *
     * @param {ComplexTable2} self table control instance reference
     * @param {number} rowIdx buffer row index
     * @param {number} columnIdx table column index
     * @param {number} controlIdx column control index
     * @param {object} columnConfig table column configuration
     * @param {HTMLElement} cellElem element to display cell content
     *
     * @returns {undefined}
     */
    function createCellControl(self, rowIdx, columnIdx, controlIdx, columnConfig, cellElem) {
        var controlConfig = shmi.cloneObject(columnConfig.fields[controlIdx].ctrl.config),
            bufferRow = self.pageBuf.rows[rowIdx],
            itemName = (bufferRow.rowId === null) ? null : self.makeItem(bufferRow.rowId, columnConfig.fields[controlIdx].dgCol),
            ia = shmi.requires("visuals.tools.item-adapter"),
            columnAdapter = self.getColumnAdapter(columnConfig.fields[controlIdx].fieldName),
            ctrl = null;

        controlConfig.item = itemName;

        ctrl = shmi.createControl(columnConfig.fields[controlIdx].ctrl["ui-type"], cellElem, controlConfig, "div");
        if (itemName !== null) {
            if (columnAdapter === null) {
                ia.unsetAdapter(itemName);
            } else {
                ia.setAdapter(itemName, columnAdapter);
            }
        }

        bufferRow.cols[columnIdx].ctrls.push(ctrl);
    }

    /**
     * createRowDescriptor - create empty row descriptor for use in page buffer
     *
     * @param {number} datagridId  dotagrid row-ID or `null` when not in use
     * @param {number} columnCount number of table columns
     *
     * @returns {object} row descriptor
     */
    function createRowDescriptor(datagridId, columnCount) {
        var rowDescr = {
            rowId: datagridId,
            trElem: null,
            trSelCtrl: null,
            selCB: null,
            cols: [],
            visible: true
        };

        for (var col = 0; col < columnCount; col++) {
            rowDescr.cols.push({ ctrls: [] });
        }

        return rowDescr;
    }

    /**
     * onQuickSearch - called to perform quicksearch filter update
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {HTMLElement} valueElement value element of quicksearch input-field
     * @return {undefined}
     */
    function onQuickSearch(self, valueElement) {
        self.qSearchInterval = self.qSearchInterval || null;
        if (self.qSearchInterval === null) {
            self.qSearchInterval = setTimeout(function() {
                if (valueElement.value !== self.qSearchOld) {
                    self.quicksearchChanged(valueElement.value);
                }
                self.qSearchOld = valueElement.value;
                self.qSearchInterval = null;
            }, 300);
        }
    }

    /**
     * setElementOffset - set partial table offset relative to scrolling area content
     *
     * @param  {HTMLElement} element partial table element
     * @param  {number} offset  top offset in px
     * @return {undefined}
     */
    function setElementOffset(element, offset) {
        var useTranslate = false,
            template = "translate(<%= X %>px, <%= Y %>px)";
        if (useTranslate) {
            element.style.transform = shmi.evalString(template, {
                X: 0,
                Y: offset
            });
        } else {
            element.style.top = offset + "px";
        }
    }

    /**
     * setPagePositions - update position of partial table elements
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {undefined}
     */
    function setPagePositions(self) {
        var tableParts = [self.tbodyElement[0], self.tbodyElement[1], self.tbodyElement[2]],
            linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
            pageHeight = self.metricsData.lineHeightPx * linesPerPage;
        self.vars.pageOrder.forEach(function(pageIndex) {
            setElementOffset(tableParts[pageIndex], self.vars.pageOffsets[pageIndex] * pageHeight);
        });
    }

    /**
     * reinitPages - reinitialize partial table elements
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {number} scrollTop current scrolling offset
     * @return {undefined}
     */
    function reinitPages(self, scrollTop) {
        var lineHeight = self.metricsData.lineHeightPx,
            totalHeight = self.pageBuf.nofTotalRows * lineHeight,
            linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
            pageHeight = self.metricsData.lineHeightPx * linesPerPage,
            numPages = Math.ceil(totalHeight / pageHeight),
            offsetPages = scrollTop / pageHeight,
            curPage = Math.floor(offsetPages);

        if ((curPage >= 1) && (numPages > 3)) {
            self.vars.pageOffsets = [curPage - 1, curPage, curPage + 1];
        } else {
            self.vars.pageOffsets = [0, 1, 2];
        }
        self.vars.pageOrder = [0, 1, 2];
        setPagePositions(self);
    }

    /**
     * turnPage - turn table element paging forwards/backwards
     *
     * @param {object} self control instance reference
     * @param {boolean} [backwards=false] `false` to page forwards, `true` for backwards
     */
    function turnPage(self, backwards = false) {
        const curPage = self.vars.pageOffsets[backwards ? self.vars.pageOrder[0] : self.vars.pageOrder[2]];
        if (backwards) {
            self.vars.pageOrder.unshift(self.vars.pageOrder.pop());
        } else {
            self.vars.pageOrder.push(self.vars.pageOrder.shift());
        }
        self.vars.pageOffsets[self.vars.pageOrder[0]] = curPage - 1;
        self.vars.pageOffsets[self.vars.pageOrder[1]] = curPage;
        self.vars.pageOffsets[self.vars.pageOrder[2]] = curPage + 1;
        setPagePositions(self);
    }

    /**
     * updatePages - update position of partial table elements when scrolling changed
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {number} scrollTop scrolling offset
     * @return {undefined}
     */
    function updatePages(self, scrollTop) {
        var lineHeight = self.metricsData.lineHeightPx,
            totalHeight = self.pageBuf.nofTotalRows * lineHeight,
            linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
            viewportHeight = self.metricsData.viewportHeight,
            pageHeight = lineHeight * linesPerPage,
            numPages = Math.ceil(totalHeight / pageHeight),
            topY = scrollTop,
            bottomY = topY + viewportHeight,
            dy = scrollTop - self.vars.scrollTop,
            direction = Math.sign(dy),
            pageThreshold = null,
            pageOffset = null,
            oldOffset = self.pageBuf.offset,
            newOffset = null,
            oldBufferOffset = self.pageBuf.bufferOffset,
            oldPageOrder = null,
            reinited = false;

        if ((numPages > 3) && (Math.abs(dy) < pageHeight / 2)) {
            //turn pages after small scroll
            if (direction === DIRECTION_UP) {
                pageOffset = self.vars.pageOffsets[self.vars.pageOrder[0]] * pageHeight;
                pageThreshold = pageOffset + (pageHeight / 2);
                if ((pageOffset > 0) && (topY < pageThreshold)) {
                    //pageUp
                    turnPage(self, true);
                }
            } else if (direction === DIRECTION_DOWN) {
                pageOffset = self.vars.pageOffsets[self.vars.pageOrder[2]] * pageHeight;
                pageThreshold = pageOffset + (pageHeight / 2);
                if ((pageOffset > 0) && (bottomY > pageThreshold)) {
                    //pageDown
                    turnPage(self, false);
                }
            }
        } else if ((numPages > 3) || self.vars.pageOffsets.some((pOffset) => pOffset > 2)) {
            //reorder after big scroll or old page offsets out of range
            oldPageOrder = self.vars.pageOrder.toString();
            reinitPages(self, scrollTop);
            reinited = true;
        }

        newOffset = Math.floor((self.vars.pageOffsets[self.vars.pageOrder[0]] * pageHeight) / lineHeight);
        self.vars.scrollTop = scrollTop;

        if (newOffset !== oldOffset) {
            self.pageBuf.offset = newOffset;
            self.pageBuf.bufferOffset = calcBufferOffset(self);
            if (oldBufferOffset !== self.pageBuf.bufferOffset) {
                startLoading(self);
                self.dgMan.setOffset(self.cConfig.table, self.pageBuf.bufferOffset, self.dgSubscription.id);
            } else {
                self.onDataGridChange(self.dgSubscription.changeInfo);
            }
        } else if (reinited && (oldPageOrder !== self.vars.pageOrder.toString())) {
            self.onDataGridChange(self.dgSubscription.changeInfo);
        }
    }

    /**
     * calcBufferOffset - calculate new (data-)buffer offset based on current (view-buffer-)offset.
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {number} new data buffer offset
     */
    function calcBufferOffset(self) {
        var newBufferOffset = self.pageBuf.bufferOffset;
        if (self.pageBuf.offset < self.pageBuf.bufferOffset) {
            newBufferOffset = Math.max(0, (self.pageBuf.offset + self.pageBuf.viewportSize) - self.pageBuf.bufferSize);
        } else if (self.pageBuf.offset + self.pageBuf.viewportSize > self.pageBuf.bufferOffset + self.pageBuf.bufferSize) {
            newBufferOffset = self.pageBuf.offset;
        }
        return newBufferOffset;
    }

    /**
     * getTableId - create unique ID to create instance specific inline stylesheet
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {string} instance ID
     */
    function getTableId(self) {
        var idPrefix = "ct2-instance-",
            numId = 0;
        while (shmi.visuals.controls.ComplexTable2.prototype._tableIds[idPrefix + numId] !== undefined) {
            numId += 1;
        }
        shmi.visuals.controls.ComplexTable2.prototype._tableIds[idPrefix + numId] = true;
        return idPrefix + numId;
    }

    /**
     * releaseTableId - release registered instance ID
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {string} tableId instance ID
     * @return {undfined}
     */
    function releaseTableId(self, tableId) {
        delete shmi.visuals.controls.ComplexTable2.prototype._tableIds[tableId];
    }

    /**
     * applyInstanceStyles - apply configuration depended instance styles
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {undefined}
     */
    function applyInstanceStyles(self) {
        const stylesheetContent = [];
        let styleSheet = null;

        if (!self.styleSheet) {
            styleSheet = document.createElement("style");
            self.styleSheet = styleSheet;
        }

        /* generate style for table row height */
        if (self.cConfig.resp["line-height"]) {
            stylesheetContent.push("#" + self.tableId + " .iqel-td-content { height: " + self.cConfig.resp["line-height"] + "; }");
        }

        /* generate style for hidden columns */
        const hiddenColumns = self.cConfig.resp["column-org"].map((col, idx) => ({
            index: idx,
            hidden: col?.fields[0]?.hidden ?? null
        })).filter((col) => !!col.hidden);

        if (hiddenColumns.length > 0) {
            const hiddenContent = [];

            hiddenColumns.forEach((col) => {
                hiddenContent.push(
                    `#${self.tableId} td:nth-child(${col.index + 1})`,
                    `#${self.tableId} th:nth-child(${col.index + 1})`,
                    `#${self.tableId} col:nth-child(${col.index + 1})`
                );
            });

            stylesheetContent.push(`${hiddenContent.join(", ")} { display: none; }`);
        }

        /* generate style for columns depending on current media query */
        const responsiveColumns = self.cConfig.resp["column-org"].map((col, idx) => ({
            index: idx,
            minMediaQuery: col?.fields[0]?.minMediaQuery ?? null
        })).filter((col) => !!col.minMediaQuery);

        if (responsiveColumns.length > 0) {
            const mqInfo = shmi.getMediaQueries(),
                mediaQueries = mqInfo.type === 1 ? mqInfo.queries.slice() : mqInfo.queries.slice().reverse();

            mediaQueries.forEach((mq) => {
                const responsiveContent = [];

                responsiveColumns.forEach((col) => {
                    if (!col.mqFound && (col.minMediaQuery !== mq.name)) {
                        responsiveContent.push(
                            `.${mq.name} #${self.tableId} td:nth-child(${col.index + 1})`,
                            `.${mq.name} #${self.tableId} th:nth-child(${col.index + 1})`,
                            `.${mq.name} #${self.tableId} col:nth-child(${col.index + 1})`
                        );
                    } else {
                        col.mqFound = true;
                    }
                });

                if (responsiveContent.length > 0) {
                    stylesheetContent.push(`${responsiveContent.join(", ")} { display: none; }`);
                }
            });
        }

        /* join generated styles into instance stylesheet */
        self.styleSheet.innerHTML = stylesheetContent.join("\n");

        if (styleSheet) {
            self.element.insertBefore(self.styleSheet, self.element.firstChild);
        }
    }

    /**
     * startLoading - start loading state
     *
     * @param {object} self control instance reference
     */
    function startLoading(self) {
        self.loadingCount += 1;

        if (self.loadingCount === 1) {
            shmi.addClass(self.element, "loading");
        }
    }

    /**
     * stopLoading - stop loading state
     *
     * @param object} self control instance reference
     */
    function stopLoading(self) {
        self.loadingCount -= 1;
        if (self.loadingCount <= 0) {
            self.loadingCount = 0;
            shmi.removeClass(self.element, "loading");
        }
    }

    /**
     * Creates a new ComplexTable2
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {Element} element the base element of the control
     * @param {Object} config control configuration
     */
    shmi.visuals.controls.ComplexTable2 = function(element, config) {
        // generate a unique id for this CT2 instance
        this.instID = Date.now();
        while (shmi.visuals.controls.ComplexTable2.instIDs.indexOf(this.instID) !== -1) {
            this.instID++;
        }
        shmi.visuals.controls.ComplexTable2.instIDs.push(this.instID);

        // short cuts to static "const" definitions for CT2
        this.c = shmi.visuals.controls.ComplexTable2.c;
        this.uiElName = shmi.visuals.controls.ComplexTable2.uiElName;
        this.uiCssCl = shmi.visuals.controls.ComplexTable2.uiCssCl;

        this.element = element;
        this.config = config || {};
        this.loadingCount = 0;
        this.lastClicked = {
            rowId: null,
            offset: 0
        };
        this.parseAttributes();

        shmi.def(this.config, "class-name", "complex-table2");
        shmi.def(this.config, "template", "default/complex-table2");
        shmi.def(this.config, "name", null);
        shmi.def(this.config, "label", "complex-table2");
        shmi.def(this.config, "table", "undefined-table");
        shmi.def(this.config, "item-selected-row", null);
        shmi.def(this.config, "rows-items-from-1", true);
        shmi.def(this.config, "field-datagrid-col-map", []);
        shmi.def(this.config, "default-field-control-map", {});
        shmi.def(this.config, "default-field-headers", {});
        shmi.def(this.config, "default-layout", {});
        shmi.def(this.config, "select-mode", "MULTI");
        shmi.def(this.config, "show-select-boxes", true);
        shmi.def(this.config, "cb-checked-icon-src", "pics/system/controls/complex-table2/cb-checkmark.svg");
        shmi.def(this.config, "cb-undefined-icon-src", "pics/system/controls/complex-table2/cb-undefined.svg");
        shmi.def(this.config, "search-label", null);
        shmi.def(this.config, "search-placeholder", "${ct2_quicksearch}");
        shmi.def(this.config, "search-template", "default/complex-table2.quick-search");
        shmi.def(this.config, "search-icon-src", "pics/system/controls/complex-table2/search.svg");
        shmi.def(this.config, "clear-icon-src", "pics/system/controls/complex-table2/clear-icon.svg");
        shmi.def(this.config, "sort-up-icon-src", "pics/system/controls/complex-table2/sort-up.svg");
        shmi.def(this.config, "sort-down-icon-src", "pics/system/controls/complex-table2/sort-down.svg");
        shmi.def(this.config, "filter-template", "default/complex-table2.filter");
        shmi.def(this.config, "filter-icon-src", "pics/system/controls/complex-table2/filter.svg");
        shmi.def(this.config, "filter-label", null);
        shmi.def(this.config, "filter-placeholder", null);
        shmi.def(this.config, "text-mode", "NORMAL");
        shmi.def(this.config, "show-nof-rows", false);
        shmi.def(this.config, "delete-selected-rows", false);
        shmi.def(this.config, "useDgIndex", false);
        shmi.def(this.config, "col-width-changeable", false);
        shmi.def(this.config, "cols-show-hide", false);
        shmi.def(this.config, "cols-movable", false);
        shmi.def(this.config, "rows-movable", false);
        shmi.def(this.config, "sortable-fields", []);
        shmi.def(this.config, "default-text-filter-fields", []);
        shmi.def(this.config, "filters", []);
        shmi.def(this.config, "default-nof-buffered-rows", this.c.MIN_BUF_SIZE);
        shmi.def(this.config, "show-buttons-table-min-width-px", 300);
        shmi.def(this.config, "edit-mode-active", false);
        shmi.def(this.config, "fieldIcons", {});
        shmi.def(this.config, "headerMode", "ICON");
        shmi.def(this.config, "adapterSelectConfig", {
            "class-name": "iq-select-box no-label",
            "label": null
        });
        shmi.def(this.config, "toggle-selection", true);
        shmi.def(this.config, "double-click-events", false);
        shmi.def(this.config, "adapterSelectClass", "adapter-select");

        //using standard "input-field" control for edit-mode
        shmi.def(this.config, "edit-mode-map", {});

        // "browser-dependencies" may be undefined
        // "responsive-layouts" may be undefined

        this.cConfig = {};
        this.filterData = {};
        this.colTextFilterData = {};
        this.ctrlPanelData = {};
        this.header = {};
        this.colTxtFilters = {};
        this.pageBuf = {};
        this.asyncProcData = {};
        this.metricsData = {};
        this.selection = {
            type: this.c.SELECT_TYPE_NOTHING,
            selRows: [],
            selRowIndex: [],
            dblClicked: false
        };
        this.lastSelection = null;
        this.scrollBar = null;
        this.dgMan = null;
        this.dgSubscription = {};
        this.selChgEventListeners = {};

        this.tcElement = null;
        this.bodyViewportElement = null;
        this.tabCCElement = null;
        this.tabHCElement = null;
        this.theadElement = null;
        this.txtFiltersHeadElement = null;
        this.tbodyElement = [];
        this.labelElement = null;
        this.footerElement = null;
        this.ctrlPanelElement = null;

        this.resizeDebounceTimeout = null;
        this.sbSliderDebounceTimeout = null;
        this.vpReleaseDebounceTimeout = null;
        this.swipeVertDebounceTimeout = null;

        this.cLayoutClass = null;
        this.isTableToCleanup = false;
        this.isTouchDevice = false;

        this.lastTableVpWidth = -1; // force init 1st time
        this.lastTableVpHeight = -1; // force init 1st time
        this.lastLayoutId = -100; // force init 1st time
        this.applyFiltersEnable = false;
        this.subscrIdItemSelRow = null;
        this.selRowByItem = 0;

        this.editMode = false;
        this.editRow = -1;

        this.headerMode = "TEXT"; // "TEXT" / "ICON"

        this.controls = [];

        this.vars = {
            lastUpdate: 0,
            updateTimeout: 0,
            scrollTop: 0,
            pageOffsets: [
                0,
                1,
                2
            ],
            pageOrder: [
                0,
                1,
                2
            ]
        };
        this.qSearchOld = null;

        this.startup();
    };

    shmi.visuals.controls.ComplexTable2.prototype = {

        /*-- BaseControl method interface --*/
        _tableIds: {},
        uiType: "complex-table2",
        isContainer: true,
        events: ["select"],
        getClassName: function() {
            return "ComplexTable2";
        },
        setEditMode: function(editModeEnabled) {
            var self = this;
            self.editMode = editModeEnabled;
        },
        /**
         * Retrieves information about the currently selected rows.
         *
         * @returns {Object} the selection info, for structure refer clearSelection method
         */
        getSelectedRows: function() {
            return shmi.cloneObject(this.selection);
        },
        /**
         * Gets the datagrid row data for the current selection
         *
         * @param {Object} selection
         * @returns {Object} the datagrid data for the selection
         */
        getSelectionRowData: function(selection) {
            var self = this,
                answer = [];
            if (selection && selection.selRows.length > 0) {
                answer = selection.selRows.map((row) => self.dgMan.getRowData(self.config.table, row));
            }
            return answer;
        },
        /**
         * Sets the current selection
         *
         * @param {Object} selection the selection to set, for structure refer clearSelection method
         */
        setSelectedRows: function(selection) {
            shmi.checkArg("selection", selection, "object");
            shmi.checkArg("selection.type", selection.type, "number");
            shmi.checkArg("selection.selRows", selection.selRows, "array");
            shmi.checkArg("selection.selRowIndex", selection.selRowIndex, "array");
            shmi.checkArg("selection.dblClicked", selection.dblClicked, "boolean", "undefined");
            Object.assign(this.selection, {
                type: this.c.SELECT_TYPE_NOTHING,
                selRows: [],
                selRowIndex: [],
                dblClicked: false
            }, shmi.cloneObject(selection));
            this.updateSelectionView();

            var self = this;
            clearTimeout(self.sel_fire_to);
            self.sel_fire_to = setTimeout(function() {
                self.fireSelChanged(shmi.cloneObject(self.selection));
            }, shmi.c("ACTION_RETRY_TIMEOUT"));
        },
        /**
         * Adds a listener for selection changes
         *
         * @param {Object} listenerObj the listener to add, must implement callback method <listener>.tableSelectionChange(selection);
         * @returns {number} the listener id
         */
        addSelChgEventListener: function(listenerObj) {
            var id = Date.now();
            while (this.selChgEventListeners[id] !== undefined) {
                id++;
            }
            this.selChgEventListeners[id] = {
                listener: listenerObj
            };
            shmi.log("[ComplexTable2] addSelChgEventListener called, listenerObj: " + listenerObj + " - id:" + id, 1);
            return id;
        },
        /**
         * Removes a listener for selection changes.
         *
         * @param {number} id the id of the listener to remove
         */
        removeSelChgEventListener: function(id) {
            if (this.selChgEventListeners[id] !== undefined) {
                delete this.selChgEventListeners[id];
            } else {
                shmi.log("[ComplexTable2] removeSelChgEventListener - id " + id + " does not exist", 2);
            }
            shmi.log("[ComplexTable2] removeSelChgEventListener called, id: " + id, 1);
        },
        onInit: function() {
            var self = this,
                i = 0;
            self.headerMode = (self.config.headerMode === "ICON") ? "ICON" : "TEXT";
            self.tableId = getTableId(self);
            self.element.setAttribute("id", self.tableId);
            if (shmi.toBoolean(self.config["edit-mode-active"]) === true) {
                self.editMode = true;
            }

            this.isTouchDevice = this.detectTouchDevice();
            if (this.isTouchDevice) {
                shmi.addClass(this.element, this.uiCssCl.touchDev);
            } else {
                shmi.addClass(this.element, this.uiCssCl.noTouchDev);
            }

            this.buildCCfgRespIndep();
            this.buildCCfgRespDep();

            /* init all members, create child controls, get references to the necessary elements and ui-controls */
            this.dgMan = shmi.visuals.session.DataGridManager;
            this.tcElement = shmi.getUiElement(this.uiElName.tabContainer, this.element);
            if (!this.tcElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tabContainer + " element provided (required)", 3);
                return;
            }

            this.theadElement = shmi.getUiElement(this.uiElName.thead, this.element);
            if (!this.theadElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.thead + " element provided (required)", 3);
                return;
            }
            this.txtFiltersHeadElement = shmi.getUiElement(this.uiElName.txtFiltersHead, this.element);
            if (!this.txtFiltersHeadElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.txtFiltersHead + " element provided (optional)", 1);
            }

            this.tableElement = shmi.getUiElement(this.uiElName.table, this.element);
            if (!this.tableElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.table + " element provided (required)", 3);
                return;
            }
            var tableBodyParts = shmi.getUiElements(this.uiElName.tbody, this.element);
            this.tbodyElement = [];
            for (i = 0; i < tableBodyParts.length; i++) {
                this.tbodyElement.push(tableBodyParts[i]);
            }
            if (!this.tbodyElement[0]) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tbody + " element provided (required)", 3);
                return;
            }
            this.bodyViewportElement = shmi.getUiElement(this.uiElName.tbodyContainer, this.element);
            if (!this.bodyViewportElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tbodyContainer + " element provided (required)", 3);
                return;
            }

            this.tabCCElement = shmi.getUiElement(this.uiElName.tabContContainer, this.element);
            if (!this.tabCCElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tabContContainer + " element provided (required)", 3);
                return;
            }

            this.labelElement = shmi.getUiElement(this.uiElName.label, this.element);
            if (!this.labelElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.label + " element provided (optional)", 1);
            } else {
                this.labelElement.textContent = shmi.localize(this.cConfig.label);
            }
            this.footerElement = shmi.getUiElement(this.uiElName.footer, this.element);
            if (!this.footerElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.footer + " element provided (optional)", 1);
            }
            this.ctrlPanelElement = shmi.getUiElement(this.uiElName.ctrlPanel, this.element);
            if (!this.ctrlPanelElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.ctrlPanelElement + " element provided (optional)", 1);
            }

            this.initScrollHandler();
            this.updateSize = () => {
                clearTimeout(this.resizeDebounceTimeout);
                this.resizeDebounceTimeout = setTimeout(() => {
                    this.resizeDebounceTimeout = null;
                    this.processEvtOnResize();
                }, this.c.DEBOUNCE_TO_RESIZE);
            };

            this.resizeObserver = new ResizeObserver(this.updateSize);

            applyInstanceStyles(self);
            this.cleanupTabContent();
            this.cleanupAsyncProcessing();
            this.clearSelection();
            this.clearFilterData();
            this.clearColTextFilterData();
            this.isTabSizeChanged(); // init last..
            this.isLayoutChanged(); // init last..

            shmi.log("[ComplexTable2] initialized", 1);

            self = this;
            var tabContCtrls;
            if (shmi.isRegistered(this.element, this.uiType)) {
                tabContCtrls = shmi.parseControls(this.tcElement, false, this.element);
                for (i = 0; i < tabContCtrls.length; i++) {
                    self.controls.push(tabContCtrls[i]);
                }
            } else {
                var l_id = this.listen('register', function() {
                    tabContCtrls = shmi.parseControls(self.tcElement, false, self.element);
                    for (i = 0; i < tabContCtrls.length; i++) {
                        self.controls.push(tabContCtrls[i]);
                    }
                    self.unlisten('register', l_id);
                });
            }

            self.adapterItems = [];
            var iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
            iterObj(self.config.adapterSettings, function(val, prop) {
                var adapterEntry = {
                    field: prop,
                    item: null,
                    gridName: self.config.table,
                    options: val
                };
                //function(name, type, min, max, value, setvalueCallback)
                var iName = "virtual:" + self.getAdapterNS() + ":" + prop;
                adapterEntry.item = shmi.createVirtualItem(iName, shmi.c("TYPE_INT"), 0, 1000, 0, self._getAdapterHandler(self, adapterEntry));
                self.adapterItems.push(adapterEntry);
            });
        },
        _getAdapterHandler: function(self, adapterEntry) {
            return function(value) {
                var adapter = null,
                    im = shmi.visuals.session.ItemManager,
                    ia = shmi.requires("visuals.tools.item-adapter"),
                    filterItems = [],
                    iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
                    fieldIdx = self.getDataColNo(adapterEntry.field);

                adapterEntry.options.forEach(function(option, idx) {
                    if (option.value === value) {
                        adapter = option.adapter;
                    }
                });

                iterObj(im.items, function(itm, itmName) {
                    if (itmName.indexOf("virtual:grid:" + adapterEntry.gridName) === 0) {
                        var nameParts = itmName.split(":");
                        if (parseInt(nameParts[4]) === fieldIdx) {
                            filterItems.push(itm);
                        }
                    }
                });

                if (adapter === null) {
                    filterItems.forEach(function(itm, idx) {
                        ia.unsetAdapter(itm.name);
                    });
                } else {
                    filterItems.forEach(function(itm, idx) {
                        ia.setAdapter(itm.name, adapter);
                    });
                }
            };
        },
        onEnable: function() {
            const self = this;
            let grid = null,
                sizesCalculated = false;

            this.enableEvents();
            this.updateRespDepContent(true);
            this.cleanupMetrics();

            if (this.cConfig.table) {
                grid = this.dgMan.getGrid(this.cConfig.table);
                if (grid) {
                    grid.clearFilter(-1);

                    if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable) {
                        if (self.cConfig.quicksearch.remember) {
                            if (typeof self.qSearchOld === "string") {
                                self.ctrlPanelData.qSearchInput.setValue(self.qSearchOld);
                                self.quicksearchChanged(self.qSearchOld);
                            }
                        }
                    }
                }
            }

            this.setApplyFiltersEnable(true);
            if (this.cConfig.table && this.dgMan.getGrid(this.cConfig.table)) {
                startLoading(self);
                this.dgSubscription = this.dgMan.subscribePage(
                    this.cConfig.table, 0, this.cConfig["buffer-size"],
                    (dgChgInfo) => {
                        if (dgChgInfo.status === "STARTLOAD") {
                            startLoading(self);
                        } else {
                            stopLoading(self);
                        }
                        if (this.testlog) console.log("***** datagridChange", dgChgInfo);
                        this.dgSubscription.changeInfo = dgChgInfo;
                        this.onDataGridChange(dgChgInfo);
                        if (!sizesCalculated) {
                            sizesCalculated = true;
                            self.processEvtOnResize();
                        }
                    });
                this.checkDgIndexFields();
                if (this.cConfig["item-selected-row"] && (this.cConfig["item-selected-row"] !== "undefined-item")) {
                    this.selRowByItem = 0; // at startup always 0
                    this.subscrIdItemSelRow = shmi.visuals.session.ItemManager.subscribeItem(this.cConfig["item-selected-row"], this);
                }
            }

            this.resizeObserver.observe(this.element);

            this.controls.forEach(function(ctrl) {
                ctrl.enable();
            });

            shmi.log("[ComplexTable2] enabled", 1);
        },
        /**
         * resetQuickSearch - reset quick search input
         *
         */
        resetQuickSearch: function() {
            var self = this;
            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable) {
                self.qSearchOld = null;
                if (self.isActive()) {
                    self.ctrlPanelData.qSearchInput.setValue("");
                    self.quicksearchChanged("");
                }
            }
        },
        /**
         * setQuickSearch - set quick search input
         *
         */
        setQuickSearch: function(value) {
            var self = this,
                setValue = value;

            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable) {
                if (setValue === undefined || setValue === null) {
                    setValue = "";
                } else if (typeof setValue !== "string") {
                    setValue = String(setValue);
                }
                if (self.isActive() && self.qSearchOld !== setValue) {
                    self.ctrlPanelData.qSearchInput.setValue(setValue);
                    self.quicksearchChanged(setValue);
                }
            }
        },
        onDisable: function() {
            if (this.pageBuf.rows) {
                this.pageBuf.rows.forEach(function(row, idx) {
                    if (row.trSelCtrl && row.trSelCtrl.rsMouseListener) {
                        row.trSelCtrl.rsMouseListener.disable();
                        row.trSelCtrl.rsTouchListener.disable();
                    }
                    if (row.selCB && row.selCB.cbMouseListener) {
                        row.selCB.cbMouseListener.disable();
                        row.selCB.cbTouchListener.disable();
                    }
                });
            }

            this.cleanupAsyncProcessing();
            if (this.dgSubscription) {
                this.dgMan.unsubscribe(this.cConfig.table, this.dgSubscription.id);
                this.dgSubscription = null;
            }
            this.disableEvents();
            this.cleanupTabContent();
            if (this.subscrIdItemSelRow) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.cConfig["item-selected-row"], this.subscrIdItemSelRow);
            }

            if (this.quicksearchHandler && this.quicksearchValueElement) {
                this.quicksearchValueElement.removeEventListener('keyup', this.quicksearchHandler);
                if (this.vars.qSearchToken) {
                    this.vars.qSearchToken.unlisten();
                }
                this.quicksearchValueElement = null;
                this.quicksearchHandler = null;
            }

            if (!(this.cConfig.quicksearch && this.cConfig.quicksearch.enable && this.cConfig.quicksearch.remember)) {
                this.qSearchOld = null;
            }

            this.selection = {
                type: this.c.SELECT_TYPE_NOTHING,
                selRows: [],
                selRowIndex: []
            };
            this.lastSelection = null;
            setLastClicked(this, -1);

            this.resizeObserver.disconnect();

            shmi.log("[ComplexTable2] disabled", 1);
        },
        onDelete: function() {
            var self = this;
            if (self.header.cols) {
                self.header.cols.forEach(function(col, idx) {
                    if (col.ctrls[0].cbMouseListener) {
                        col.ctrls[0].cbMouseListener.disable();
                        col.ctrls[0].cbTouchListener.disable();
                    }
                });
            }
            if (self.ctrlPanelData.filter) {
                self.ctrlPanelData.filter.items.forEach(function(item, idx) {
                    shmi.visuals.session.ItemManager.removeItem(item.name);
                });
            }
            if (self.onBodyScroll && self.bodyViewportElement) {
                self.bodyViewportElement.removeEventListener('scroll', self.onBodyScroll);
            }
            releaseTableId(self, self.tableId);
            self.element.removeAttribute("id");
        },

        /*-- DataGrid callback method interface --*/

        onDataGridChange: function(dgChgInfo) {
            var self = this;
            if (dgChgInfo && (dgChgInfo.status === "OK")) {
                if (self.isTableToCleanup) {
                    self.cleanupTabContent();
                    self.isTableToCleanup = false;
                }
                var currDgIds = self.dgMan.getCurrentIDs(self.cConfig.table, self.dgSubscription.id);

                if (self.pageBuf.rows.length === self.pageBuf.viewportSize) {
                    if ((currDgIds.length <= self.pageBuf.bufferSize) && (dgChgInfo.offset === self.pageBuf.bufferOffset)) {
                        self.pageBuf.size = currDgIds.length;
                        if (self.pageBuf.nofTotalRows !== dgChgInfo.totalRows) {
                            self.pageBuf.nofTotalRows = dgChgInfo.totalRows;
                            self.tableElement.style.height = (self.pageBuf.nofTotalRows * self.metricsData.lineHeightPx) + "px";
                        }
                        self.updateTable(dgChgInfo, currDgIds); // change item refs only, reconnect
                    } else {
                        self.cleanupTabContent();
                        self.reinitTable(dgChgInfo, currDgIds, true);
                    }
                } else {
                    self.cleanupTabContent();
                    self.reinitTable(dgChgInfo, currDgIds);
                }
                self.findSelectionAfterRefresh();
                self.updateNofRowsDisplay();
                self.updateSelectionView();
            }
        },

        // callback interface for optional items
        onSetValue: function(value, type, name) {
            var rowVal = -1;

            if (name === this.cConfig["item-selected-row"]) {
                if (value !== -1) {
                    if (this.cConfig["rows-items-from-1"]) {
                        rowVal = value - 1;
                    } else {
                        rowVal = value;
                    }
                }
                this.selRowByItem = rowVal;
                this.updateSelectionView();
            }
        },

        /*-- private helper methods - usage within CT2 only !! --*/

        /* config processors */
        /* note: use this.cConfig instead of this.Config only! Some properties are pre-processed for
                 a faster access at runtime. this.cConfig is divided in a responsive-independent and responsive-
                 dependent part, the last one may be changed at runtime!!
          */
        /**
         * @private
         */
        buildCCfgRespIndep: function() {
            this.cConfig.name = this.config.name;
            this.cConfig.label = this.config.label;
            this.cConfig["class-name"] = this.config["class-name"];
            this.cConfig.template = this.config.template;
            this.cConfig.table = this.config.table;
            this.cConfig["item-selected-row"] = this.config["item-selected-row"];
            this.cConfig["rows-items-from-1"] = this.config["rows-items-from-1"];
            if (this.config["field-datagrid-col-map"]) {
                this.cConfig["field-datagrid-col-map"] = this.config["field-datagrid-col-map"];
            } else {
                this.cConfig["field-datagrid-col-map"] = null;
            }
            this.cConfig["delete-selected-rows"] = shmi.toBoolean(this.config["delete-selected-rows"]);
            this.cConfig.filters = this.config.filters;
            this.cConfig.quicksearch = this.config.quicksearch;
            this.cConfig["nof-buffered-rows"] = shmi.toNumber(this.config["default-nof-buffered-rows"]);
            if (this.config["buffer-size"] && this.config["buffer-size"] > this.c.MIN_BUF_SIZE) {
                this.cConfig["buffer-size"] = this.config["buffer-size"];
            } else {
                this.cConfig["buffer-size"] = this.c.MIN_BUF_SIZE;
            }

            if (this.config["delete-selected-rows-button-config"]) {
                this.cConfig["delete-selected-rows-button-config"] = this.config["delete-selected-rows-button-config"];
            }
            if (this.config["default-filter-button-config"]) {
                this.cConfig["default-filter-button-config"] = this.config["default-filter-button-config"];
            }

            if (this.config["browser-dependencies"]) {
                var browserDeps = this.config["browser-dependencies"],
                    userAgent = navigator.userAgent;
                for (var i = 0; i < browserDeps.length; i++) {
                    var regEx = new RegExp(browserDeps[i].userAgentDetectRegEx);
                    if (regEx.test(userAgent)) {
                        if (browserDeps[i]["nof-buffered-rows"]) {
                            this.cConfig["nof-buffered-rows"] = shmi.toNumber(browserDeps[i]["nof-buffered-rows"]);
                        }
                        break;
                    }
                }
            }

            this.cConfig["show-buttons-table-min-width-px"] = shmi.toNumber(this.config["show-buttons-table-min-width-px"]);

            // nof-buffered-rows must be even
            var bufSize = this.cConfig["nof-buffered-rows"],
                sizeTemp = 0;
            if ((bufSize % 3) !== 0) {
                sizeTemp = Math.ceil(bufSize / 3) * 3;
                this.cConfig["nof-buffered-rows"] = sizeTemp;
                if (this.cConfig["nof-buffered-rows"] > this.cConfig["buffer-size"]) {
                    this.cConfig["buffer-size"] = this.cConfig["nof-buffered-rows"];
                }
                console.info("[ComplexTable2] nof-buffered-rows must be a multiple of 3, has been corrected to " + sizeTemp);
            }
        },
        /**
         * @private
         */
        buildCCfgRespDep: function() {
            const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
            var i;
            // init the default resp config
            this.cConfig.resp = {};
            this.cConfig.resp["v-scroll-options"] = this.config["v-scroll-options"];
            this.cConfig.resp["h-scroll-options"] = this.config["h-scroll-options"];
            if (this.cConfig["item-selected-row"] && (this.cConfig["item-selected-row"] !== "undefined-item")) {
                this.cConfig.resp["select-mode"] = "ITEM";
            } else {
                this.cConfig.resp["select-mode"] = this.config["select-mode"];
            }
            this.cConfig.resp["text-mode"] = this.config["text-mode"];
            this.cConfig.resp["show-nof-rows"] = this.config["show-nof-rows"];
            this.cConfig.resp["col-width-changeable"] = this.config["col-width-changeable"];
            this.cConfig.resp["cols-show-hide"] = this.config["cols-show-hide"];
            this.cConfig.resp["cols-movable"] = this.config["cols-movable"];
            this.cConfig.resp["rows-movable"] = this.config["rows-movable"];
            this.cConfig.resp["sortable-fields"] = this.config["sortable-fields"];
            this.cConfig.resp["text-filter-fields"] = this.config["default-text-filter-fields"];
            this.cConfig.resp["toggle-selection"] = (this.config["toggle-selection"] ?? true) || (this.cConfig.resp["select-mode"] === "MULTI");
            this.cConfig.resp["double-click-events"] = (this.config["double-click-events"] ?? false) && !this.cConfig.resp["toggle-selection"];
            if (this.cConfig.quicksearch) {
                if (this.cConfig.quicksearch.enable) {
                    this.cConfig.resp["text-filter-fields"] = this.cConfig.quicksearch.fields;
                }
            }
            if (this.config["default-text-filter-input-field-config"]) {
                this.cConfig.resp["text-filter-input-field-config"] = this.config["default-text-filter-input-field-config"];
            }
            if (this.config["default-text-filter-clear-button-config"]) {
                this.cConfig.resp["text-filter-clear-button-config"] = this.config["default-text-filter-clear-button-config"];
            }
            //--
            var defLayout = this.config["default-layout"];
            if (defLayout["class-name"]) {
                this.cConfig.resp["layout-class-name"] = defLayout["class-name"];
            } else {
                this.cConfig.resp["layout-class-name"] = null;
            }

            if (defLayout["line-height"]) {
                this.cConfig.resp["line-height"] = defLayout["line-height"];
            } else {
                this.cConfig.resp["line-height"] = "var(--table-row-height)";
            }
            // example of this.cConfig.resp["column-org"]
            //[
            //    {
            //        "fields": [
            //            { "dgCol": 0, "header": "headCol1Fld0", "isSortable": true, "hasTextFilter": true, "ctrl":
            // { "ui-type": "text2", "config": { "config-name": "ct2-text2" } } },
            //            { "dgCol": 2, "header": "headCol1Fld1", "isSortable": false, "hasTextFilter": false, "ctrl":
            // { "ui-type": "text2", "config": { "config-name": "ct2-text2" } } }
            //        ],
            //        "hasTextFilter": true // is true if at least one field of the ct2 column has a text filter
            //    },
            //    {
            //        "fields": [
            //            { "dgCol": 5, "header": "headCol2Fld0", "isSortable": true, "hasTextFilter": false, "ctrl":
            // { "ui-type": "text2", "config": { "config-name": "ct2-text2" } } }
            //        ],
            //        "hasTextFilter": false // is true if at least one field of the ct2 column has a text filter
            //    }
            //]
            this.cConfig.resp["column-org"] = [];
            var cfgSrcColOrg = defLayout["column-org"],
                cCfgDestColOrg = this.cConfig.resp["column-org"],
                nofSrcCols = 0,
                colNo;

            iterObj(cfgSrcColOrg, (srcCol, col) => {
                // assume that col is of format col<number>, e.g. "col1", "col17"
                if (col.substr(0, 3).toLowerCase() === "col") {
                    colNo = parseInt(col.substring(3)) - 1; // 0..nofCols-1
                    const destCol = cCfgDestColOrg[colNo] = {},
                        srcFields = srcCol.fields,
                        nofFields = srcFields.length;
                    let colHasTextFilter = false;

                    destCol.fields = [];

                    for (i = 0; i < nofFields; i++) {
                        destCol.fields[i] = {};

                        destCol.fields[i]["min-width"] = srcCol["min-width"];
                        destCol.fields[i]["column-width"] = srcCol["column-width"];
                        destCol.fields[i].minMediaQuery = srcCol.minMediaQuery;
                        destCol.fields[i].hidden = srcCol.hidden;
                        destCol.fields[i].fieldName = srcFields[i];
                        destCol.fields[i].dgCol = this.getDataColNo(srcFields[i]);
                        destCol.fields[i].header = this.getHeaderCaption(srcFields[i]);
                        destCol.fields[i].icon = this.config.fieldIcons[srcFields[i]];
                        destCol.fields[i].isSortable = this.getFldSortable(srcFields[i]);
                        destCol.fields[i].hasTextFilter = this.getFldHasTextFilter(srcFields[i]);
                        if (destCol.fields[i].hasTextFilter) {
                            colHasTextFilter = true;
                        }
                        destCol.fields[i].ctrl = this.getCtrlDef(srcFields[i]);
                    }
                    destCol.hasTextFilter = colHasTextFilter;
                    nofSrcCols++;
                }
            });

            var nofCols = cCfgDestColOrg.length;
            this.cConfig.resp["nof-cols"] = nofCols;
            if (nofCols !== nofSrcCols) {
                shmi.log("[ComplexTable2] buildCCfgRespDep - error in default-layout: there must be defined col1 till coln without gaps and duplicates", 3);
            }
            this.cConfig.resp["layout-id"] = -1; // default layout

            // overwrite the default resp config with "responsive-layouts" settings if necessary
            if (this.config["responsive-layouts"]) {
                var mLayout = null,
                    cWidth = this.getTableWidth(),
                    respLayouts = this.config["responsive-layouts"],
                    nofRespLayouts = respLayouts.length;
                for (i = 0; i < nofRespLayouts; i++) {
                    if (cWidth <= respLayouts[i]["table-max-width-px"]) {
                        mLayout = respLayouts[i];
                        this.cConfig.resp["layout-id"] = i;
                        break;
                    }
                }
                if (mLayout) {
                    if (mLayout.headerMode === "ICON") {
                        //self.headerMode = "ICON";
                        this.cConfig.resp.headerMode = "ICON";
                    } else {
                        //self.headerMode = "TEXT";
                        this.cConfig.resp.headerMode = "TEXT";
                    }

                    if (mLayout["line-height"]) {
                        this.cConfig.resp["line-height"] = mLayout["line-height"];
                    }

                    if (mLayout["v-scroll-options"] !== undefined) {
                        this.cConfig.resp["v-scroll-options"] = mLayout["v-scroll-options"];
                    }
                    if (mLayout["h-scroll-options"] !== undefined) {
                        this.cConfig.resp["h-scroll-options"] = mLayout["h-scroll-options"];
                    }
                    if (mLayout["select-mode"] !== undefined) {
                        this.cConfig.resp["select-mode"] = mLayout["select-mode"];
                    }
                    if (mLayout["text-mode"] !== undefined) {
                        this.cConfig.resp["text-mode"] = mLayout["text-mode"];
                    }
                    if (mLayout["show-nof-rows"] !== undefined) {
                        this.cConfig.resp["show-nof-rows"] = mLayout["show-nof-rows"];
                    }
                    if (mLayout["col-width-changeable"] !== undefined) {
                        this.cConfig.resp["col-width-changeable"] = mLayout["col-width-changeable"];
                    }
                    if (mLayout["cols-show-hide"] !== undefined) {
                        this.cConfig.resp["cols-show-hide"] = mLayout["cols-show-hide"];
                    }
                    if (mLayout["cols-movable"] !== undefined) {
                        this.cConfig.resp["cols-movable"] = mLayout["cols-movable"];
                    }
                    if (mLayout["rows-movable"] !== undefined) {
                        this.cConfig.resp["rows-movable"] = mLayout["rows-movable"];
                    }
                    if (mLayout["sortable-fields"] !== undefined) {
                        this.cConfig.resp["sortable-fields"] = mLayout["sortable-fields"];
                    }
                    if (mLayout["class-name"] !== undefined) {
                        this.cConfig.resp["layout-class-name"] = mLayout["class-name"];
                    }
                    if (mLayout["text-filter-fields"] !== undefined) {
                        this.cConfig.resp["text-filter-fields"] = mLayout["text-filter-fields"];
                    }
                    if (mLayout["text-filter-input-field-config"] !== undefined) {
                        this.cConfig.resp["text-filter-input-field-config"] = mLayout["text-filter-input-field-config"];
                    }
                    if (mLayout["text-filter-clear-button-config"] !== undefined) {
                        this.cConfig.resp["text-filter-clear-button-config"] = mLayout["text-filter-input-field-config"];
                    }

                    cfgSrcColOrg = mLayout["column-org"];
                    cCfgDestColOrg = this.cConfig.resp["column-org"];

                    iterObj(cfgSrcColOrg, (srcCol, col) => {
                        // assume that col is of format col<number>, e.g. "col1", "col17"
                        if (col.substr(0, 3).toLowerCase() === "col") {
                            colNo = parseInt(col.substring(3)) - 1; // 0..nofCols-1
                            srcCol = cfgSrcColOrg[col];
                            if ((typeof srcCol === "string") || (srcCol instanceof String)) {
                                // copy src colNo to dest colNo
                                var srcColNo = parseInt(srcCol.substring(3)) - 1; // 0..nofCols-1
                                cCfgDestColOrg[colNo] = cCfgDestColOrg[srcColNo];
                            } else {
                                const destCol = cCfgDestColOrg[colNo] = {},
                                    srcFields = srcCol.fields,
                                    nofFields = srcFields.length;
                                let colHasTextFilter = false;

                                destCol.fields = [];

                                for (i = 0; i < nofFields; i++) {
                                    destCol.fields[i] = {};
                                    destCol.fields[i].fieldName = srcFields[i];
                                    destCol.fields[i].dgCol = this.getDataColNo(srcFields[i]);
                                    var respHeaders = null;
                                    if (srcCol["field-headers"]) {
                                        respHeaders = srcCol["field-headers"];
                                    }
                                    destCol.fields[i].header = this.getHeaderCaption(srcFields[i], respHeaders);
                                    destCol.fields[i].icon = this.config.fieldIcons[srcFields[i]];
                                    destCol.fields[i].isSortable = this.getFldSortable(srcFields[i]);
                                    destCol.fields[i].hasTextFilter = this.getFldHasTextFilter(srcFields[i]);
                                    if (destCol.fields[i].hasTextFilter) {
                                        colHasTextFilter = true;
                                    }
                                    var respColCtrlMap = null;
                                    if (srcCol["field-control-map"]) {
                                        respColCtrlMap = srcCol["field-control-map"];
                                    }
                                    destCol.fields[i].ctrl = this.getCtrlDef(srcFields[i], respColCtrlMap);
                                }
                                destCol.hasTextFilter = colHasTextFilter;
                            }
                        }
                    });

                    nofCols = colNo + 1;
                    if (nofCols < cCfgDestColOrg.length) {
                        // cleanup col defs for col >= nofCols
                        cCfgDestColOrg.splice(nofCols, cCfgDestColOrg.length - nofCols);
                        this.cConfig.resp["nof-cols"] = nofCols;
                    } else if (nofCols > this.cConfig.resp["nof-cols"]) {
                        shmi.log("[ComplexTable2] buildCCfgRespDep, it is impossible to extent the number of columns in responsive-layouts", 3);
                    }
                }
            }
        },
        /**
         * @private
         */
        checkDgIndexFields: function() {
            var grid = this.dgMan.getGrid(this.cConfig.table);
            if (grid && (typeof grid.getIndexFields === "function")) {
                this.cConfig.indexFields = grid.getIndexFields();
                if (this.cConfig.indexFields.length > 0) {
                    this.cConfig.useDgIndex = true;
                }
            } else {
                shmi.log("[ComplexTable2] No index found in datagrid", 1);
            }
        },
        /**
         * @private
         */
        getDataColNo: function(fldName) {
            var colNo = this.cConfig["field-datagrid-col-map"][fldName];
            if (colNo === undefined) {
                colNo = 0;
                shmi.log("[ComplexTable2] getDataColNo, unknown fldName " + fldName, 3);
            } else {
                colNo = shmi.toNumber(colNo);
            }
            return colNo;
        },
        /**
         * @private
         */
        getFldSortable: function(fldName) {
            var sortable = false;
            if (this.cConfig.resp["sortable-fields"].indexOf(fldName) !== -1) {
                sortable = true;
            }
            return sortable;
        },
        /**
         * @private
         *
         * @param {string} fldName
         * @returns {boolean} has text filter true/false
         */
        getFldHasTextFilter: function(fldName) {
            var hasTxtFilter = false;
            if (this.cConfig.resp["text-filter-fields"].indexOf(fldName) !== -1) {
                hasTxtFilter = true;
            }
            return hasTxtFilter;
        },
        /**
         * @private
         *
         * @param {string} fldName
         * @param {Object} [ctrlMap] may be null. If defined it's checked for fldName at 1st.
         * @returns {Object}
         */
        getCtrlDef: function(fldName, ctrlMap) {
            var ctrlDef;
            if (ctrlMap) {
                ctrlDef = ctrlMap[fldName];
                if (!ctrlDef) {
                    ctrlDef = this.config["default-field-control-map"][fldName];
                }
            } else {
                ctrlDef = this.config["default-field-control-map"][fldName];
            }
            if (!ctrlDef) {
                ctrlDef = {};
                shmi.log("[ComplexTable2] getCtrlDef, unknown fldName " + fldName, 3);
            }
            return ctrlDef;
        },
        /**
         * @private
         *
         * @param {string} fldName
         * @param {Object} [headerMap] may be null. If defined it's checked for fldName at 1st.
         * @returns {string} caption, may be null
         */
        getHeaderCaption: function(fldName, headerMap) {
            var caption;
            if (headerMap) {
                caption = headerMap[fldName];
                if (caption === null) {
                    return null; // legal case: e.g. the default caption is overwritten by null in a responsive layout
                } else if (!caption) {
                    caption = this.config["default-field-headers"][fldName];
                }
            } else {
                caption = this.config["default-field-headers"][fldName];
            }
            if (!caption) {
                caption = "";
                shmi.log("[ComplexTable2] getHeaderCaption, unknown fldName " + fldName, 3);
            }
            return caption;
        },
        /*
         * @private
         */
        getColumnAdapter: function(fldName) {
            var self = this,
                adapter = null;
            if (self.config.adapterSettings && (self.config.adapterSettings[fldName] !== undefined)) {
                /* {
                    field: prop,
                    item: null,
                    gridName: self.config.table,
                    options: val
                } */
                var settings = self.config.adapterSettings[fldName],
                    curVal = null;
                self.adapterItems.forEach(function(aEntry, idx) {
                    if (aEntry.field === fldName) {
                        curVal = aEntry.item.readValue();
                    }
                });
                settings.forEach(function(entry, idx) {
                    if (curVal === entry.value) {
                        adapter = entry.adapter;
                    }
                });
            }
            return adapter;
        },
        // *ConfigVal may be null or undefined
        // returns null if both keys do not exist in config
        /**
         * @private
         */
        getChildCtrlConfig: function(primConfigVal, secConfigVal) {
            var cfg = null;
            if (primConfigVal) {
                cfg = shmi.cloneObject(primConfigVal);
            } else if (secConfigVal) {
                cfg = shmi.cloneObject(secConfigVal);
            }
            return cfg;
        },
        /**
         * @private
         */
        processAsyncMetricsUpdate: function(scrollTop) {
            var self = this,
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            self.controls.forEach(function(ctrl, idx) {
                if (!ctrl.isActive()) {
                    var t = tm.createTask("table-control-#" + (idx + 1));
                    t.run = function() {
                        var enableTok = ctrl.listen("enable", function(evt) {
                            enableTok.unlisten();
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            tl = tm.createTaskList(tasks, false);

            tl.onComplete = function() {
                var tHeight = 0;
                if (self.active) {
                    self.updateMetricsData(scrollTop);
                    self.processTabReinitDependencies();

                    if (typeof scrollTop === "number") {
                        tHeight = self.tableElement.getBoundingClientRect().height;
                        if (tHeight >= scrollTop + self.metricsData.viewportHeight) {
                            self.bodyViewportElement.scrollTop = scrollTop;
                        } else {
                            self.bodyViewportElement.scrollTop = Math.max(0, tHeight - self.metricsData.viewportHeight);
                        }
                    }
                }
            };

            if (tasks.length === 0) {
                tl.onComplete();
            } else {
                tl.run();
            }
        },
        /**
         * @private
         */
        processEvtOnResize: function() {
            var self = this;
            if (this.initialized) {
                this.buildCCfgRespDep();
                applyInstanceStyles(self);
                if (this.isTabSizeChanged()) {
                    // do the same table processing as onInit -> onEnable
                    this.currentScrollEvtType = this.c.SCROLL_EVT_TYPE_RESIZE;
                    this.cleanupTabContent();
                    this.cleanupAsyncProcessing();
                    this.findSelectionAfterRefresh();
                    this.clearFilterData();
                    this.clearColTextFilterData();
                    this.updateRespDepContent(true);
                    this.cleanupMetrics();
                    // force onDataGridChange callback in every case:
                    if (this.dgSubscription && this.dgSubscription.changeInfo) {
                        self.cleanupTabContent();
                        self.reinitTable(this.dgSubscription.changeInfo, self.dgMan.getCurrentIDs(self.cConfig.table, self.dgSubscription.id));
                    }
                }
                //}
            }
        },
        /* select processing */
        /**
         * @private
         */
        clearSelection: function() {
            var self = this;
            this.selection.type = this.c.SELECT_TYPE_NOTHING; // || .._ALL || .._SEL_ROWS
            this.selection.selRows = [];
            this.selection.selRowIndex = [];
            this.selection.dblClicked = false;

            if (self.editRow !== -1) {
                self.resetBufferRow(self.editRow);
                self.editRow = -1;
            }
            clearTimeout(self.sel_fire_to);
            self.sel_fire_to = setTimeout(function() {
                self.fireSelChanged(shmi.cloneObject(self.selection));
            }, shmi.c("ACTION_RETRY_TIMEOUT"));
        },
        /**
         * @private
         */
        findSelectionAfterRefresh: function() {
            const self = this,
                grid = self.dgMan.getGrid(self.cConfig.table);

            if (grid && this.cConfig.useDgIndex) {
                if (this.selection.type === this.c.SELECT_TYPE_SEL_ROWS) {
                    const selectionClone = shmi.cloneObject(this.selection);

                    selectionClone.selRowIndex.forEach(function(el, idx) {
                        const newRow = grid.searchIndexRowId(el);
                        if (newRow === -1) {
                            self.removeRowIdFromSelection(selectionClone.selRows[idx]);
                        } else if (newRow !== selectionClone.selRows[idx]) {
                            const currentSelectionIndex = self.selection.selRows.indexOf(selectionClone.selRows[idx]);
                            if (currentSelectionIndex > -1) {
                                self.selection.selRows[currentSelectionIndex] = newRow;
                            }
                        }
                    });
                }

                if (this.selection && this.selection.selRows.length > 0) {
                    this.selection.type = this.c.SELECT_TYPE_SEL_ROWS;
                } else {
                    this.selection.type = this.c.SELECT_TYPE_NOTHING;
                }
                this.selection.dblClicked = false;

                if (self.editRow !== -1) {
                    self.resetBufferRow(self.editRow);
                    self.editRow = -1;
                }
                clearTimeout(self.sel_fire_to);
                self.sel_fire_to = setTimeout(function() {
                    self.fireSelChanged(shmi.cloneObject(self.selection));
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            } else {
                this.clearSelection();
            }
        },
        /**
         * @private
         */
        updateSelectionView: function() {
            if (this.testlog) console.log("*** update selection view", this.selection.selRows, this.selection.selRowIndex.toString());
            var isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                isSingleSelectMode = (this.cConfig.resp["select-mode"] === "SINGLE"),
                isItemSelectMode = (this.cConfig.resp["select-mode"] === "ITEM");
            if (isItemSelectMode) {
                this.setAllRowSelectors(this.c.CB_UNCHK);
                this.setRowSelectorByDgRow(this.selRowByItem, this.c.CB_CHK);
            } else {
                switch (this.selection.type) {
                case this.c.SELECT_TYPE_NOTHING:
                    if (isMultiSelectMode) {
                        this.setAllCheckBoxes(this.c.CB_UNCHK);
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                    } else if (isSingleSelectMode) {
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                    }
                    break;
                case this.c.SELECT_TYPE_ALL:
                    if (isMultiSelectMode) {
                        this.setAllCheckBoxes(this.c.CB_CHK);
                        this.setAllRowSelectors(this.c.CB_CHK);
                    } else {
                        shmi.log("[ComplexTable2] updateSelectionView, it is impossible to select all rows in single select mode", 2);
                    }
                    break;
                default: // .._SEL_ROWS
                    if (isMultiSelectMode) {
                        this.setAllCheckBoxes(this.c.CB_UNCHK);
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                        if (this.header.selAllCBox) {
                            this.header.selAllCBox.setState(this.c.CB_UNDEF);
                        }
                        var currDgIds = this.dgMan.getCurrentIDs(this.cConfig.table, this.dgSubscription.id);
                        for (var i = 0; i < this.selection.selRows.length; i++) {
                            if (currDgIds.indexOf(this.selection.selRows[i]) !== -1) {
                                this.setCheckBox(this.selection.selRows[i], this.c.CB_CHK);
                                this.setRowSelector(this.selection.selRows[i], this.c.CB_CHK);
                            }
                        }
                    } else if (isSingleSelectMode) {
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                        /*if (this.selection.selRows.length > 1) {
                            shmi.log("[ComplexTable2] updateSelectionView, it is impossible to select more than one row in single select mode", 2);
                        }*/
                        /*if (this.selection.selRows.length === 1) {
                            this.setRowSelector(this.selection.selRows[0], this.c.CB_CHK);
                        }*/
                        this.selection.selRows.forEach(function(el) {
                            this.setRowSelector(el, this.c.CB_CHK);
                        }.bind(this));
                    }
                    break;
                }
            }
        },
        /**
         * @private
         */
        setAllCheckBoxes: function(selVal) {
            var self = this;
            if (self.header.selAllCBox) {
                self.header.selAllCBox.setState(selVal);
            }
            self.pageBuf.rows.forEach(function(r) {
                if (r.selCB) {
                    r.selCB.setState(selVal);
                }
            });
        },
        /**
         * @private
         */
        setCheckBox: function(rowId, selVal) {
            var bufRow = this.searchRowId(rowId);
            if (bufRow !== -1) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].selCB) {
                    this.pageBuf.rows[bufRow].selCB.setState(selVal);
                }
            }
        },
        /**
         * @private
         */
        setAllRowSelectors: function(selVal) {
            var self = this;
            self.pageBuf.rows.forEach(function(r) {
                if (r.trSelCtrl) {
                    r.trSelCtrl.setState(selVal);
                }
            });
        },
        /**
         * @private
         */
        setRowSelector: function(rowId, selVal) {
            var self = this,
                bufRow = this.searchRowId(rowId);
            if (bufRow !== -1) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].trSelCtrl) {
                    this.pageBuf.rows[bufRow].trSelCtrl.setState(selVal);
                    if (self.editMode) {
                        var cc = shmi.requires("visuals.tools.control-config");
                        if (self.editRow !== -1) {
                            self.resetBufferRow(self.editRow);
                        }
                        self.editRow = bufRow;
                        this.pageBuf.rows[bufRow].cols.forEach(function(colEntry, idx) {
                            colEntry.ctrls.forEach(function(ctrl, jdx) {
                                var cfgCol = self.cConfig.resp["column-org"][idx],
                                    cConf = cc.getConfig(ctrl),
                                    iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
                                if (self.config['edit-mode-map'][cfgCol.fields[jdx].fieldName] !== undefined) {
                                    var editCfg = shmi.cloneObject(self.config['edit-mode-map'][cfgCol.fields[jdx].fieldName]);
                                    iterObj(editCfg, function(val, prop) {
                                        cConf[prop] = val;
                                    });
                                    var lTok = shmi.listen('control-reconfiguration', function(evt) {
                                        lTok.unlisten();
                                        colEntry.ctrls[jdx] = evt.detail.control;
                                    }, {
                                        "detail.oldControl": ctrl
                                    });
                                    cc.setConfig(ctrl, cConf);
                                }
                            });
                        });
                        self.updateSize();
                    }
                }
            }
        },
        /**
         * @private
         */
        resetBufferRow: function(bufRow) {
            var self = this,
                cc = shmi.requires("visuals.tools.control-config");
            if (self.pageBuf.rows[bufRow] === undefined) {
                self.editRow = -1;
                return;
            }
            self.pageBuf.rows[bufRow].cols.forEach(function(colEntry, idx) {
                var cfgCol = self.cConfig.resp["column-org"][idx];
                colEntry.ctrls.forEach(function(ctrl, jdx) {
                    var oldCfg = cc.getConfig(ctrl),
                        cfg = shmi.cloneObject(cfgCol.fields[jdx].ctrl.config);
                    cfg.item = oldCfg.item;
                    cfg.ui = cfgCol.fields[jdx].ctrl["ui-type"];
                    shmi.removeClass(ctrl.element, oldCfg["class-name"]);
                    var lTok = shmi.listen('control-reconfiguration', function(evt) {
                        lTok.unlisten();
                        colEntry.ctrls[jdx] = evt.detail.control;
                    }, {
                        "detail.oldControl": ctrl
                    });
                    cc.setConfig(ctrl, cfg);
                });
            });
        },
        /**
         * @private
         */
        setRowSelectorByDgRow: function(dgRow, selVal) {
            var bufRow = dgRow - this.pageBuf.offset;
            if ((bufRow >= 0) && (bufRow < this.pageBuf.viewportSize)) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].trSelCtrl) {
                    this.pageBuf.rows[bufRow].trSelCtrl.setState(selVal);
                }
            }
        },
        /**
         * @private
         */
        // ..SEL_ROWS and bufRow === -1 means "do not change selected rows"
        updateSelectionData: function(type, bufRow, state, shiftPressed, ctrlPressed, fromCheckbox, dblClicked) {
            var self = this,
                isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                isSingleSelectMode = (this.cConfig.resp["select-mode"] === "SINGLE"),
                isItemSelectMode = (this.cConfig.resp["select-mode"] === "ITEM");

            if (isItemSelectMode) {
                // write always - independent of chk/unchk
                var im = shmi.visuals.session.ItemManager;
                var selRowVal = this.pageBuf.offset + bufRow;
                var selRowsCopy = null;
                if (this.cConfig["rows-items-from-1"]) {
                    selRowVal++;
                }
                im.writeValue(this.cConfig["item-selected-row"], selRowVal);
            } else {
                switch (type) {
                case this.c.SELECT_TYPE_ALL:
                    if (!isMultiSelectMode) {
                        console.warn("ComplexTable2", "updateSelectionData called with type SELECT_TYPE_ALL but table not in multi-select mode.");
                    }
                    // fallthrough
                case this.c.SELECT_TYPE_NOTHING:
                    this.selection.type = type;
                    this.selection.selRows = [];
                    this.selection.selRowIndex = [];
                    this.selection.dblClicked = dblClicked;
                    break;
                default: // .._SEL_ROWS
                    if (bufRow !== -1) {
                        if (isMultiSelectMode) {
                            if (shiftPressed && this.lastClicked.rowId > -1) {
                                if (!ctrlPressed) {
                                    this.selection.selRows = [];
                                    this.selection.selRowIndex = [];
                                }
                                addSelectionRange(this, this.pageBuf.rows[bufRow].rowId).then().catch((e) => {
                                    shmi.notify("Error fetching selection data: " + e.message, "${V_ERROR}");
                                });
                                this.selection.lastClickedRow = bufRow;
                                setLastClicked(this, this.pageBuf.rows[bufRow].rowId);
                                return;
                            } else if (fromCheckbox || ctrlPressed) {
                                if (state === this.c.CB_CHK) {
                                    this.addRowIdToSelection(this.pageBuf.rows[bufRow].rowId);
                                } else {
                                    this.removeRowIdFromSelection(this.pageBuf.rows[bufRow].rowId);
                                }
                                this.selection.lastClickedRow = bufRow;
                                setLastClicked(this, this.pageBuf.rows[bufRow].rowId);
                            } else {
                                selRowsCopy = shmi.cloneObject(self.selection.selRows);
                                selRowsCopy.forEach(function(rid) {
                                    self.removeRowIdFromSelection(rid);
                                });
                                if (state === this.c.CB_CHK) {
                                    self.addRowIdToSelection(this.pageBuf.rows[bufRow].rowId);
                                }
                                setLastClicked(this, this.pageBuf.rows[bufRow].rowId);
                            }
                        } else if (isSingleSelectMode) {
                            this.selection.selRows = [];
                            this.selection.selRowIndex = [];

                            if (state === this.c.CB_CHK) {
                                this.addRowIdToSelection(this.pageBuf.rows[bufRow].rowId);
                            } else {
                                this.removeRowIdFromSelection(this.pageBuf.rows[bufRow].rowId);
                            }
                        }
                    }
                    this.selection.dblClicked = dblClicked;
                    this.updateSelTypeFromSelRows();
                    break;
                }

                clearTimeout(self.sel_fire_to);
                self.sel_fire_to = setTimeout(function() {
                    self.fireSelChanged(shmi.cloneObject(self.selection));
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            }
        },
        /**
         * @private
         */
        getRowIndex: function(rowId) {
            var rowIndex = null,
                grid = this.dgMan.getGrid(this.cConfig.table);

            if (grid) {
                rowIndex = grid.getRowIndex(rowId);
            }

            return rowIndex;
        },
        /**
         * @private
         */
        addRowIdToSelection: function(rowId) {
            if (this.selection.selRows.indexOf(rowId) === -1) {
                this.selection.selRows.push(rowId);
                if (this.cConfig.useDgIndex) {
                    var rowIndex = this.getRowIndex(rowId);
                    this.selection.selRowIndex.push(rowIndex);
                }
            }
        },
        /**
         * @private
         */
        removeRowIdFromSelection: function(rowId) {
            var self = this,
                idx = this.selection.selRows.indexOf(rowId);
            if (idx !== -1) {
                this.selection.selRows.splice(idx, 1);
                if (this.cConfig.useDgIndex) {
                    this.selection.selRowIndex.splice(idx, 1);
                }
            } else if (self.editRow !== -1) {
                self.resetBufferRow(self.editRow);
                self.editRow = -1;
                self.updateSize();
            }
        },
        /**
         * @private
         */
        updateSelTypeFromSelRows: function() {
            if (this.selection.selRows.length === this.pageBuf.nofTotalRows) {
                this.selection.type = this.c.SELECT_TYPE_SEL_ROWS;
            } else if (this.selection.selRows.length === 0) {
                this.selection.type = this.c.SELECT_TYPE_NOTHING;
            } else {
                this.selection.type = this.c.SELECT_TYPE_SEL_ROWS;
            }
        },
        /**
         * @private
         */
        selectAllCBoxValChanged: function(state) {
            var type;
            switch (state) {
            case this.c.CB_UNCHK:
                type = this.c.SELECT_TYPE_NOTHING;
                break;
            case this.c.CB_CHK:
                type = this.c.SELECT_TYPE_ALL;
                loadRange(this, -1, 0, this.dgSubscription.changeInfo.totalRows, () => {
                    /* all rows loaded */
                });
                return;
            default:
                type = this.c.SELECT_TYPE_SEL_ROWS;
                break;
            }
            this.updateSelectionData(type, -1, this.c.CB_UNDEF, false, false, true, false);
            this.updateSelectionView();
        },
        /**
         * @private
         */
        selectCBoxValChanged: function(bufRow, state, shiftPressed, ctrlPressed, fromCheckbox, dblClicked) {
            if (this.c.MULTI_SELECT_MODE === "3-state-clear") {
                if (this.header.selAllCBox && this.header.selAllCBox.getState() === this.c.CB_UNCHK) {
                    this.clearSelection();
                }
            }
            this.updateSelectionData(this.c.SELECT_TYPE_SEL_ROWS, bufRow, state, shiftPressed, ctrlPressed, fromCheckbox, dblClicked);
            this.updateSelectionView();
        },
        /**
         * @private
         */
        selectRowValChanged: function(bufRow, state, shiftPressed, ctrlPressed, dblClicked) {
            this.updateSelectionData(this.c.SELECT_TYPE_SEL_ROWS, bufRow, state, shiftPressed, ctrlPressed, false, dblClicked);
            this.updateSelectionView();
        },
        /**
         * @private
         */
        fireSelChanged: function(selection) {
            const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

            iterObj(this.selChgEventListeners, (listener) => {
                listener.listener.tableSelectionChange(selection);
            });

            if (!this.lastSelection || selection.dblClicked || !isEqualSelection(this.lastSelection, selection)) {
                this.lastSelection = shmi.cloneObject(selection);
                this.updateNofRowsDisplay();
                this.fire("select", selection);
            }
        },

        /* sort processing */
        /**
         * @private
         */
        sortHeaderChanged: function(dgCol, state) {
            var sortCol,
                dir;
            switch (state) {
            case this.c.SORT_UP:
                sortCol = dgCol;
                dir = "DESC";
                break;
            case this.c.SORT_DOWN:
                sortCol = dgCol;
                dir = "ASC";
                break;
            default:
                sortCol = -1;
                dir = "ASC";
                break;
            }
            setLastClicked(this, -1);
            this.dgMan.sort(this.cConfig.table, sortCol, dir);
            this.clearSortHeaderExcept(dgCol);
            this.clearSelection();
        },
        /**
         * @private
         */
        clearSortHeaderExcept: function(dgCol) {
            var nofCols = this.cConfig.resp["nof-cols"];
            for (var col = 0; col < nofCols; col++) {
                var hCtrls = this.header.cols[col].ctrls;
                for (var i = 0; i < hCtrls.length; i++) {
                    if (hCtrls[i].id !== dgCol) {
                        hCtrls[i].setState(this.c.NO_SORT);
                    }
                }
            }
        },

        /* filter processing */
        /**
         * @private
         */
        txtFiltersUsed: function() {
            var txtFlUsed = false;
            if (this.config["default-text-filter-fields"].length > 0) {
                txtFlUsed = true;
            }
            return txtFlUsed;
        },
        /**
         * @private
         */
        clearFilterData: function() {
            this.filterData = {};
            var nofFilters = this.cConfig.filters.length;
            for (var fId = 0; fId < nofFilters; fId++) {
                var fCfg = this.cConfig.filters[fId],
                    dgColProp = this.getDataColNo(fCfg.field).toString(),
                    fData = this.filterData[dgColProp];
                if (!fData) {
                    fData = {};
                    fData.expr = [];
                    fData.changed = false;
                    this.filterData[dgColProp] = fData;
                }
            }
        },
        /**
         * @private
         */
        clearColTextFilterData: function() {
            this.colTextFilterData = {};
            var colTxtFilterFields = this.cConfig.resp["text-filter-fields"];
            for (var fIdx = 0; fIdx < colTxtFilterFields.length; fIdx++) {
                var dgColProp = this.getDataColNo(colTxtFilterFields[fIdx]).toString(),
                    fData = this.colTextFilterData[dgColProp];
                if (!fData) {
                    fData = {};
                    fData.expr = "";
                    fData.changed = false;
                    this.colTextFilterData[dgColProp] = fData;
                }
            }
        },
        /**
         * @private
         */
        quicksearchChanged: function(value) {
            var fExprVal = (typeof value === "string") ? value.trim() : value;
            var fields = this.cConfig.quicksearch.fields;
            if (fExprVal === "") {
                this.removeQuicksearchFilterExpr(fields);
            } else {
                this.setQuicksearchFilterExpr(fExprVal, fields);
            }
            this.applyFilters();
            this.clearSelection();
            this.resetScroll();
        },
        /**
         * @private
         */
        colTxtFilterChanged: function(value, type, vItemName) {
            var fExprVal = (typeof value === "string") ? value.trim() : value;
            if (fExprVal === "") {
                var fCol = this.getFilterId(vItemName);
                this.removeColTxtFilterExpr(fCol);
            } else {
                this.setColTxtFilterExpr(vItemName, fExprVal);
            }
            this.applyFilters();
            this.clearSelection();
            this.resetScroll();
        },
        /**
         * @private
         */
        colTxtFilterReset: function(fCol) {
            var colTxtFilItem = this.colTxtFilters.items[fCol];
            if (colTxtFilItem) {
                var im = shmi.visuals.session.ItemManager;
                im.writeValue(colTxtFilItem.name, "");
            }
        },
        /**
         * @private
         */
        setFilterExpr: function(vItemName) { // if not yet set !!
            var fId = this.getFilterId(vItemName),
                fIdInt = Number(fId);
            if (fIdInt !== -1) {
                var fCfg = this.cConfig.filters[fId];
                if (fCfg) {
                    var fCfgExpr = fCfg.expr,
                        dgColProp = this.getDataColNo(fCfg.field).toString(),
                        fData = this.filterData[dgColProp];
                    if (fData.expr.indexOf(fCfgExpr) === -1) {
                        fData.expr.push(fCfgExpr);
                        fData.changed = true;
                    }
                }
            }
        },
        /**
         * @private
         */
        removeFilterExpr: function(vItemName) { // if exists !!
            var fId = this.getFilterId(vItemName),
                fIdInt = Number(fId);
            if (fIdInt !== -1) {
                var fCfg = this.cConfig.filters[fId];
                if (fCfg) {
                    var fCfgExpr = fCfg.expr,
                        dgColProp = this.getDataColNo(fCfg.field).toString(),
                        fData = this.filterData[dgColProp],
                        exprIdx = fData.expr.indexOf(fCfgExpr);
                    if (exprIdx !== -1) {
                        fData.expr.splice(exprIdx, 1);
                        fData.changed = true;
                    }
                }
            }
        },
        /**
         * @private
         */
        setColTxtFilterExpr: function(vItemName, vItemValue) {
            var col = this.getFilterId(vItemName),
                dgColNos = this.getColTxtFilterDgCols(col);
            for (var i = 0; i < dgColNos.length; i++) {
                var dgColProp = dgColNos[i].toString(),
                    fData = this.colTextFilterData[dgColProp];
                if (fData) {
                    fData.expr = "%" + vItemValue + "%";
                    fData.changed = true;
                } else {
                    shmi.log("[ComplexTable2] setColTxtFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                }
            }
        },
        /**
         * @private
         */
        removeColTxtFilterExpr: function(col) {
            var dgColNos = this.getColTxtFilterDgCols(col);
            for (var i = 0; i < dgColNos.length; i++) {
                var dgColProp = dgColNos[i].toString(),
                    fData = this.colTextFilterData[dgColProp];
                if (fData) {
                    fData.expr = "";
                    fData.changed = true;
                } else {
                    shmi.log("[ComplexTable2] removeColTxtFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                }
            }
        },
        /**
         * @private
         */
        setQuicksearchFilterExpr: function(value, fields) {
            fields.forEach(function(fname) {
                var dgColNos = this.getColTxtFilterDgCols(this.getDataColIndex(fname));
                for (var i = 0; i < dgColNos.length; i++) {
                    var dgColProp = dgColNos[i].toString(),
                        fData = this.colTextFilterData[dgColProp];
                    if (fData) {
                        fData.expr = "%" + value + "%";
                        fData.changed = true;
                    } else {
                        shmi.log("[ComplexTable2] setQuicksearchFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                    }
                }
            }.bind(this));
        },
        /**
         * @private
         */
        removeQuicksearchFilterExpr: function(fields) {
            fields.forEach(function(fname) {
                var dgColNos = this.getColTxtFilterDgCols(this.getDataColIndex(fname));
                for (var i = 0; i < dgColNos.length; i++) {
                    var dgColProp = dgColNos[i].toString(),
                        fData = this.colTextFilterData[dgColProp];
                    if (fData) {
                        fData.expr = "";
                        fData.changed = true;
                    } else {
                        shmi.log("[ComplexTable2] removeQuicksearchFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                    }
                }
            }.bind(this));
        },
        /**
         * @private
         */
        getDataColIndex: function(fname) {
            var i = 0;
            var colNo;

            for (var e in this.cConfig["field-datagrid-col-map"]) {
                if (e === fname) {
                    colNo = i;
                }
                i++;
            }

            if (colNo === undefined) {
                colNo = 0;
                shmi.log("[ComplexTable2] getDataColNo, unknown fldName " + fname, 3);
            } else {
                colNo = shmi.toNumber(colNo);
            }
            return colNo;
        },
        /**
         * @private
         */
        getFilterId: function(vItemName) {
            var id = -1,
                pos = vItemName.lastIndexOf(":"),
                posInt = Number(pos);
            if (posInt !== -1) {
                id = shmi.toNumber(vItemName.substring(pos + 1));
            } else {
                shmi.log("[ComplexTable2] getFilterId, unexpected format of virtual item name: " + vItemName, 3);
            }
            return id;
        },
        /**
         * @private
         */
        getColTxtFilterDgCols: function(col) {
            var fDgCols = [],
                cfgCol = this.cConfig.resp["column-org"][col];
            if (cfgCol) {
                var nofFields = cfgCol.fields.length;
                for (var fldIdx = 0; fldIdx < nofFields; fldIdx++) {
                    if (cfgCol.fields[fldIdx].hasTextFilter) {
                        fDgCols.push(cfgCol.fields[fldIdx].dgCol);
                    }
                }
            }
            return fDgCols;
        },
        /**
         * @private
         */
        applyFilters: function() {
            if (this.isApplyFiltersEnabled()) {
                var dgBtnFilCols = Object.keys(this.filterData),
                    dgTxtFilCols = Object.keys(this.colTextFilterData),
                    dgSumFilCols = dgBtnFilCols.slice(); // clone it
                for (var i = 0; i < dgTxtFilCols.length; i++) {
                    if (dgSumFilCols.indexOf(dgTxtFilCols[i]) === -1) { // not yet contained
                        dgSumFilCols.push(dgTxtFilCols[i]);
                    }
                }

                for (i = 0; i < dgSumFilCols.length; i++) {
                    var btnFilProps = this.filterData[dgSumFilCols[i]],
                        txtFilProps = this.colTextFilterData[dgSumFilCols[i]],
                        processFilterCol = false;
                    if (btnFilProps && btnFilProps.changed) {
                        processFilterCol = true;
                    } else if (txtFilProps && txtFilProps.changed) {
                        processFilterCol = true;
                    }
                    if (processFilterCol) {
                        var fExpr = [];
                        if (btnFilProps) {
                            fExpr = btnFilProps.expr.slice();
                            btnFilProps.changed = false;
                        }
                        if (txtFilProps) {
                            if (txtFilProps.expr !== "") {
                                fExpr.push(txtFilProps.expr);
                            }
                            txtFilProps.changed = false;
                        }
                        var dgCol = shmi.toNumber(dgSumFilCols[i]);
                        if (fExpr.length === 0) {
                            setLastClicked(this, -1);
                            this.dgMan.clearFilter(this.cConfig.table, dgCol);
                        } else {
                            setLastClicked(this, -1);
                            this.dgMan.setFilter(this.cConfig.table, dgCol, fExpr);
                        }
                    }
                }
            }
        },
        /**
         * @private
         */
        isApplyFiltersEnabled: function() {
            return this.applyFiltersEnable;
        },
        /**
         * @private
         */
        setApplyFiltersEnable: function(enable) {
            this.applyFiltersEnable = enable;
        },
        /**
         * @private
         */
        resetFilterChangeFlags: function() {
            var dgBtnFilCols = Object.keys(this.filterData);
            for (var i = 0; i < dgBtnFilCols.length; i++) {
                this.filterData[dgBtnFilCols[i]].changed = false;
            }
        },
        /**
         * @private
         */
        resetColTxtFilterChangeFlags: function() {
            var dgTxtFilCols = Object.keys(this.colTextFilterData);
            for (var i = 0; i < dgTxtFilCols.length; i++) {
                this.colTextFilterData[dgTxtFilCols[i]].changed = false;
            }
        },

        /* other helpers */
        /**
         * @private
         */
        clearCtrlPanelData: function() {
            if (!this.ctrlPanelData.filter) { // 1st time
                this.ctrlPanelData.filter = {};
                this.ctrlPanelData.filter.names = [];
                this.ctrlPanelData.filter.items = [];
            }

            // reset filter items
            var restoreApplyFiltersEnable = this.isApplyFiltersEnabled();
            this.setApplyFiltersEnable(false);
            var im = shmi.visuals.session.ItemManager;
            for (var i = 0; i < this.ctrlPanelData.filter.items.length; i++) {
                im.writeValue(this.ctrlPanelData.filter.items[i].name, 0);
            }
            this.resetFilterChangeFlags();
            this.setApplyFiltersEnable(restoreApplyFiltersEnable);

            if (this.ctrlPanelData.filter.btns) {
                for (i = 0; i < this.ctrlPanelData.filter.btns.length; i++) {
                    shmi.deleteControl(this.ctrlPanelData.filter.btns[i]);
                }
            }
            this.ctrlPanelData.filter.btns = [];
        },
        /**
         * @private
         */
        clearColTextFilterCtrls: function() {
            if (!this.colTxtFilters.items) { // 1st time
                this.colTxtFilters.items = []; // 0..maxCols of this instance - on demand extended
            }

            // reset filter items
            var restoreApplyFiltersEnable = this.isApplyFiltersEnabled();
            this.setApplyFiltersEnable(false);
            var im = shmi.visuals.session.ItemManager;
            for (var i = 0; i < this.colTxtFilters.items.length; i++) {
                if (this.colTxtFilters.items[i]) {
                    im.writeValue(this.colTxtFilters.items[i].name, "");
                }
            }
            this.resetColTxtFilterChangeFlags();
            this.setApplyFiltersEnable(restoreApplyFiltersEnable);

            var ctrls = [];
            if (this.colTxtFilters.cols) {
                for (var col = 0; col < this.colTxtFilters.cols.length; col++) {
                    if (this.colTxtFilters.cols[col]) {
                        ctrls = this.colTxtFilters.cols[col].ctrls;
                        for (i = 0; i < ctrls.length; i++) {
                            shmi.deleteControl(ctrls[i]);
                        }
                    }
                }
            }
            this.colTxtFilters.cols = []; // 0..nofCols of current layout
        },
        /**
         * @private
         */
        clearCtrlPanelDOM: function() {
            var self = this;
            if (self.ctrlPanelElement) {
                if (self.ctrlPanelData && self.ctrlPanelData.qSearchInput) {
                    shmi.deleteControl(self.ctrlPanelData.qSearchInput);
                    self.ctrlPanelData.qSearchInput = null;
                }
                var childEl = self.ctrlPanelElement.firstChild;
                while (childEl) {
                    self.ctrlPanelElement.removeChild(childEl);
                    childEl = self.ctrlPanelElement.firstChild;
                }
            }
        },
        /**
         * @private
         */
        updateCtrlPanel: function() {
            if (this.ctrlPanelElement) {
                var cWidth = this.getTableWidth();
                if (cWidth < this.cConfig["show-buttons-table-min-width-px"]) {
                    shmi.removeClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelNormal);
                    shmi.addClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelMin);
                } else {
                    shmi.removeClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelMin);
                    shmi.addClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelNormal);
                }

                this.clearCtrlPanelData();
                this.clearCtrlPanelDOM();

                if (this.cConfig.quicksearch && this.cConfig.quicksearch.enable) {
                    this.enableQuickSearch();
                }
            }
        },
        /**
         * @private
         */
        enableQuickSearch: function() {
            var self = this;
            if (self.cConfig.quicksearch.chaining === "AND" || self.cConfig.quicksearch.chaining === "OR") {
                shmi.visuals.session.DataGridManager.setFilterChaining(self.cConfig.table, self.cConfig.quicksearch.chaining);
            } else {
                shmi.visuals.session.DataGridManager.setFilterChaining(self.cConfig.table, "OR");
            }

            if (self.ctrlPanelData.qSearchInput) {
                return; //no need to setup quick search input-field again when it already exists
            }

            var qSearchElem = document.createElement("div"),
                qSearchClearElem = document.createElement("div");

            shmi.addClass(qSearchElem, "qsearch-box");
            self.ctrlPanelElement.appendChild(qSearchElem);
            var conf = {
                "label": self.config["search-label"],
                "placeholder": self.config["search-placeholder"],
                "class-name": "iqel-quicksearch-input",
                "unit-text": null,
                "template": self.config["search-template"],
                "show-icon": !!self.config["search-icon-src"],
                "icon-src": self.config["search-icon-src"],
                "inline-svg-icon": true
            };
            self.ctrlPanelData.qSearchInput = shmi.createControl("iq-input-field", qSearchElem, conf, "div");

            shmi.requires("visuals.tools.icons").applyIcon(qSearchClearElem, self.config["clear-icon-src"], null, true);
            shmi.addClass(qSearchClearElem, "iqel-clear-quicksearch");
            qSearchElem.appendChild(qSearchClearElem);

            const qSearchHandler = function() {
                if (self.isActive() && !self.locked) {
                    self.ctrlPanelData.qSearchInput.setValue("");
                    self.qSearchOld = null;
                    self.quicksearchHandler();
                }
            };

            qSearchClearElem.addEventListener('click', qSearchHandler);
            qSearchClearElem.addEventListener('touchstart', qSearchHandler);

            shmi.onActive([self.ctrlPanelData.qSearchInput], function() {
                if (self.isActive()) {
                    var valueElement = self.ctrlPanelData.qSearchInput.vars.inputEl;

                    if (self.qSearchOld !== null) {
                        self.ctrlPanelData.qSearchInput.setValue(self.qSearchOld);
                    }

                    self.quicksearchHandler = onQuickSearch.bind(null, self, valueElement);
                    self.quicksearchValueElement = valueElement;
                    valueElement.addEventListener('keyup', self.quicksearchHandler);
                    self.vars.qSearchToken = self.ctrlPanelData.qSearchInput.listen("change", self.quicksearchHandler);

                    if (self.cConfig.quicksearch && self.cConfig.quicksearch.initialFocus) {
                        self.ctrlPanelData.qSearchInput.vars.inputEl.focus();
                    }
                }
            });

            if (self.isLocked()) {
                self.ctrlPanelData.qSearchInput.lock();
            }
        },
        /**
         * @private
         */
        getColTxtFilterItemNS: function() {
            return "ct2-col-txt-filter-" + this.instID;
        },
        getAdapterNS: function() {
            return "ct2-adapter-" + this.instID;
        },
        /**
         * @private
         */
        updateHeader: function() {
            var self = this;
            // cleanup this.header
            if (this.header.cols) {
                this.header.cols.forEach(function(col, idx) {
                    if (col.ctrls[0].cbMouseListener) {
                        col.ctrls[0].cbMouseListener.disable();
                        col.ctrls[0].cbTouchListener.disable();
                    }
                });
            }
            this.header.selAllCBox = null;
            this.header.cols = [];
            // cleanup DOM
            var childElem = this.theadElement.firstChild;
            while (childElem) {
                this.theadElement.removeChild(childElem);
                childElem = this.theadElement.firstChild;
            }
            // reinit header table
            var nofCols = this.cConfig.resp["nof-cols"],
                isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                colGroupElem = this.createColGroup(nofCols, isMultiSelectMode);
            this.theadElement.appendChild(colGroupElem);
            var trElem = document.createElement("tr"),
                thElem = null,
                tabCellElem = null;
            if (isMultiSelectMode && this.config["show-select-boxes"]) {
                thElem = document.createElement("th");
                tabCellElem = document.createElement("div");
                this.header.selAllCBox = this.createSelCBox(tabCellElem, -1, true, this, self.config["cb-checked-icon-src"], self.config["cb-undefined-icon-src"]);
                this.header.selAllCBox.setChangeCallback(function(id, state) {
                    if (!self.locked) {
                        self.selectAllCBoxValChanged(state);
                    }
                });
                thElem.appendChild(tabCellElem);
                trElem.appendChild(thElem);
            }
            this.theadElement.appendChild(trElem);
            for (var col = 0; col < nofCols; col++) {
                var cfgCol = this.cConfig.resp["column-org"][col];
                thElem = document.createElement("th");
                tabCellElem = document.createElement("div");
                this.header.cols[col] = {};
                this.header.cols[col].ctrls = this.createHeaderCtrls(tabCellElem, col);
                thElem.appendChild(tabCellElem);
                trElem.appendChild(thElem);

                if (cfgCol.fields[0]["min-width"]) {
                    tabCellElem.style.minWidth = cfgCol.fields[0]["min-width"];
                }
                this.header.cols[col].ctrls.forEach(function(hCtrl, idx) {
                    var fldName = hCtrl.fieldName;
                    if (self.config.adapterSettings && self.config.adapterSettings[fldName]) {
                        var settings = self.config.adapterSettings[fldName],
                            itemName = null;
                        self.adapterItems.forEach(function(aEntry, index) {
                            if (aEntry.field === fldName) {
                                itemName = aEntry.item.name;
                            }
                        });

                        var selectElem = document.createElement('div');
                        thElem.appendChild(selectElem);
                        var selectConfig = shmi.cloneObject(self.config.adapterSelectConfig);
                        selectConfig.item = itemName;
                        selectConfig.options = settings;
                        shmi.createControl("iq-select-box", selectElem, selectConfig, 'DIV', 'from');
                        shmi.addClass(thElem, self.config.adapterSelectClass);
                    }
                });
            }
        },
        /**
         * @private
         */
        updateColTextFilters: function() {
            var self = this;
            if (this.txtFiltersHeadElement && this.txtFiltersUsed()) {
                // cleanup filter data
                this.clearColTextFilterCtrls();
                // cleanup DOM
                var childElem = this.txtFiltersHeadElement.firstChild;
                while (childElem) {
                    this.txtFiltersHeadElement.removeChild(childElem);
                    childElem = this.txtFiltersHeadElement.firstChild;
                }

                // init the virtual items
                var fData = this.colTxtFilters,
                    col;
                if (fData.items.length === 0) { // 1st time
                    var maxCols = Object.keys(this.cConfig["field-datagrid-col-map"]).length, // maxCols === number of fields
                        itemNamespace = this.getColTxtFilterItemNS();
                    for (col = 0; col < maxCols; col++) {
                        var iName = "virtual:" + itemNamespace + ":" + col;
                        this.colTxtFilters.items[col] = shmi.createVirtualItem(iName, 0, 0, 1, "", function(value, type, vItemName) {
                            this.colTxtFilterChanged(value, type, vItemName);
                        }.bind(this));
                    }
                }

                // reinit filter header table
                var nofCols = this.cConfig.resp["nof-cols"],
                    isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                    colGroupElem = self.createColGroup(nofCols, isMultiSelectMode);

                this.txtFiltersHeadElement.appendChild(colGroupElem);
                var trElem = document.createElement("tr");
                this.txtFiltersHeadElement.appendChild(trElem);
                var thElem = null,
                    tabCellElem = null;
                if (isMultiSelectMode) {
                    thElem = document.createElement("th");
                    trElem.appendChild(thElem);
                }
                for (col = 0; col < nofCols; col++) {
                    var cfgCol = this.cConfig.resp["column-org"][col];
                    thElem = document.createElement("th");
                    tabCellElem = document.createElement("div");
                    thElem.appendChild(tabCellElem);
                    trElem.appendChild(thElem);
                    if (cfgCol.hasTextFilter) {
                        this.colTxtFilters.cols[col] = {};
                        this.colTxtFilters.cols[col].ctrls = this.createColTxtFilterCtrls(tabCellElem, col);
                    }
                }
            }
        },
        /**
         * @private
         */
        getDgFilterSettings: function() {
            // clear all filters and sort for the associated grid - async.
            var self = this;
            clearTimeout(self.reset_sort_filters_to);
            self.reset_sort_filters_to = setTimeout(function() {
                self.resetSortFilters();
            }, shmi.c("ACTION_RETRY_TIMEOUT"));
        },
        /**
         * @private
         */
        getColTxtFilterVal: function(fExpr) {
            var txtFlVal = "",
                exprLen = fExpr.length;
            if ((exprLen >= 3) && (fExpr.charAt(0) === "%") && (fExpr.charAt(exprLen - 1) === "%")) {
                txtFlVal = fExpr.slice(1, exprLen - 1);
            } else {
                shmi.log("[ComplexTable2] getColTxtFilterVal, unexpected format of filter expression: " + fExpr, 2);
            }
            return txtFlVal;
        },
        /**
         * @private
         */
        getBtnFilter: function(fExpr, dgCol) {
            var btnFId = -1,
                fCfg = this.cConfig.filters;
            for (var fId = 0; fId < fCfg.length; fId++) {
                var fDgCol = this.getDataColNo(fCfg[fId].field);
                if ((fDgCol === dgCol) && (fExpr === fCfg[fId].expr)) {
                    btnFId = fId;
                    break;
                }
            }
            return btnFId;
        },
        /**
         * @private
         */
        resetSortFilters: function() {
            setLastClicked(this, -1);
            this.dgMan.sort(this.cConfig.table, -1, "ASC");
            this.dgMan.clearFilter(this.cConfig.table, -1);
        },
        /**
         * @private
         */
        updateNofRowsDisplay: function() {
            if (this.cConfig.resp["show-nof-rows"] && this.footerElement) {
                this.footerElement.textContent = this.pageBuf.nofTotalRows + " " + shmi.localize("${ct2_nof_rows}");
            }
        },
        /**
         * @private
         */
        updateRespDepContent: function(layoutChanged) {
            var self = this;
            if (layoutChanged) {
                if (self.cConfig.resp.headerMode === "ICON") {
                    self.headerMode = "ICON";
                } else {
                    self.headerMode = "TEXT";
                }

                if (this.cLayoutClass) {
                    shmi.removeClass(this.element, this.cLayoutClass);
                }
                this.cLayoutClass = this.cConfig.resp["layout-class-name"];
                if (this.cLayoutClass) {
                    shmi.addClass(this.element, this.cLayoutClass);
                }
                if (this.cConfig.resp["show-nof-rows"]) {
                    shmi.removeClass(this.element, this.uiCssCl.rowDisplInvisible);
                    shmi.addClass(this.element, this.uiCssCl.rowDisplVisible);
                } else {
                    shmi.removeClass(this.element, this.uiCssCl.rowDisplVisible);
                    shmi.addClass(this.element, this.uiCssCl.rowDisplInvisible);
                }
                if (this.cConfig.resp["select-mode"] === "MULTI") {
                    shmi.addClass(this.element, this.uiCssCl.multiselect);
                } else {
                    shmi.removeClass(this.element, this.uiCssCl.multiselect);
                }
            }
            this.updateCtrlPanel();
            this.updateHeader();
            if (this.cConfig.quicksearch) {
                if (!this.cConfig.quicksearch.enable) {
                    this.updateColTextFilters();
                }
            } else {
                this.updateColTextFilters();
            }
        },

        /**
         * @private
         */
        cleanupTabContent: function() {
            this.clearPgBuf();
            this.clearTableDOM();
        },
        /**
         * @private
         */
        clearPgBuf: function() {
            if (this.pageBuf.rows) { // obj is already initialized
                for (var bufRow = 0; bufRow < this.pageBuf.rows.length; bufRow++) {
                    if (this.pageBuf.rows[bufRow].trSelCtrl) {
                        if (this.pageBuf.rows[bufRow].trSelCtrl.rsMouseListener) {
                            this.pageBuf.rows[bufRow].trSelCtrl.rsMouseListener.disable();
                            this.pageBuf.rows[bufRow].trSelCtrl.rsTouchListener.disable();
                        }
                    }
                    for (var col = 0; col < this.pageBuf.nofCols; col++) {
                        var ctrls = this.pageBuf.rows[bufRow].cols[col].ctrls;
                        for (var ctrlIdx = 0; ctrlIdx < ctrls.length; ctrlIdx++) {
                            shmi.deleteControl(ctrls[ctrlIdx]);
                        }
                    }
                }
            }
            this.pageBuf.offset = 0;
            this.pageBuf.nofTotalRows = 0;
            this.pageBuf.size = 0;
            this.pageBuf.nofCols = 0;
            this.pageBuf.firstVpRow = 0; // 0 .. .size-1
            this.pageBuf.avgNofVpLines = 0;
            this.pageBuf.rows = [];
        },
        /**
         * @private
         */
        clearTableDOM: function() {
            this.tbodyElement.forEach(function(el) {
                var childEl = el.firstChild;
                while (childEl) {
                    el.removeChild(childEl);
                    childEl = el.firstChild;
                }
            });
        },
        /**
         * @private
         */
        cleanupMetrics: function() {
            if (this.metricsData.isValid) {
                this.metricsData.last = shmi.cloneObject(this.metricsData);
            } else {
                this.metricsData.last = {};
            }
            this.metricsData.isValid = false;
            this.metricsData.lineHeightPx = 0;
        },
        /**
         * @private
         */
        cleanupAsyncProcessing: function() {
            if (this.asyncProcData.onDgChgRetryTO) {
                clearTimeout(this.asyncProcData.onDgChgRetryTO);
            }
            if (this.asyncProcData.onEvtResizeRetryTO) {
                clearTimeout(this.asyncProcData.onEvtResizeRetryTO);
            }
            if (this.asyncProcData.processMetricsTO) {
                clearTimeout(this.asyncProcData.processMetricsTO);
            }
            this.asyncProcData.onDgChgRetryTO = null;
            this.asyncProcData.onEvtResizeRetryTO = null;
            this.asyncProcData.processMetricsTO = null;
        },

        /**
         * @private
         */
        reinitTable: function(dataGridChgInfo, currentDataGridIds, keepScrollTop) {
            var self = this,
                initialSize = this.pageBuf.rows.length;
            // precondition: table + pageBuf ist empty (cleanupTabContent has already been processed)
            if (initialSize !== 0) {
                console.warn("[ComplexTable2] reinitTable, table is not empty, length:", initialSize);
                return;
            }

            if (dataGridChgInfo.totalRows !== 0) {
                shmi.removeClass(this.element, this.uiCssCl.emptyTab);

                //-- init pageBuf --
                this.pageBuf.bufferOffset = dataGridChgInfo.offset;
                this.pageBuf.nofTotalRows = dataGridChgInfo.totalRows;
                self.tableElement.style.height = (self.pageBuf.nofTotalRows * self.metricsData.lineHeightPx) + "px";
                this.pageBuf.size = currentDataGridIds.length; //number of data rows currently loaded
                this.pageBuf.viewportSize = this.cConfig["nof-buffered-rows"]; //total number of available data rows
                this.pageBuf.bufferSize = this.cConfig["buffer-size"];
                if (this.pageBuf.offset < this.pageBuf.bufferOffset) {
                    this.pageBuf.offset = this.pageBuf.bufferOffset;
                } else if (this.pageBuf.offset > this.pageBuf.bufferOffset + this.pageBuf.bufferSize) {
                    this.pageBuf.offset = this.pageBuf.bufferOffset;
                }
                var nofCols = this.pageBuf.nofCols = this.cConfig.resp["nof-cols"];
                for (var bufRow = initialSize; bufRow < this.pageBuf.viewportSize; bufRow++) {
                    //NOTE - here all row descriptors should be initialized & controls created,
                    // but controls cannot yet be connected to items and rows should be hidden
                    this.pageBuf.rows[bufRow] = createRowDescriptor((typeof currentDataGridIds[bufRow] === "number") ? currentDataGridIds[bufRow] : null, nofCols);
                }

                if (!keepScrollTop) {
                    this.bodyViewportElement.scrollTop = 0; // set scrollTop back to top of table
                }

                //-- create table DOM and cell controls --
                var isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI");

                this.tbodyElement.forEach(function(el, idx) {
                    var rowsPerPage = self.pageBuf.viewportSize / 3,
                        colGroupElem = self.createColGroup(nofCols, isMultiSelectMode);
                    el.appendChild(colGroupElem);
                    for (var i = 0; i < rowsPerPage; i++) {
                        createTableRow(self, (idx * rowsPerPage) + i, el, i === 0);
                    }
                });

                this.processAsyncMetricsUpdate(keepScrollTop ? self.vars.scrollTop : null);
            } else {
                this.pageBuf.nofCols = this.cConfig.resp["nof-cols"];
                shmi.addClass(this.element, this.uiCssCl.emptyTab);
            }
        },
        /**
         * @private
         */
        processTabReinitDependencies: function() {
            this.updateNofRowsDisplay();
            this.updateSelectionView();
        },
        doScrollUpdate: function doScrollUpdate() {
            var self = this,
                scrollTop = self.bodyViewportElement.scrollTop;

            updatePages(self, scrollTop);
        },
        /*
         * @private
         */
        initScrollHandler: function() {
            var self = this,
                scrollUpdateTimeoutLength = 100;
            self.bodyViewportElement.style["overflow-y"] = "scroll";
            self.onBodyScroll = function(evt) {
                clearTimeout(self.vars.updateTimeout);
                self.vars.updateTimeout = 0;

                if (!self.isActive() || self.locked) {
                    //reset scroll - event is not cancelable with preventDefault()
                    self.bodyViewportElement.scrollTop = 0;
                    return;
                }

                if (Date.now() - self.vars.lastUpdate > scrollUpdateTimeoutLength) {
                    self.doScrollUpdate();
                    self.vars.lastUpdate = Date.now();
                } else {
                    self.vars.updateTimeout = setTimeout(function() {
                        self.doScrollUpdate();
                        self.vars.lastUpdate = Date.now();
                    }, scrollUpdateTimeoutLength);
                }
            };
            self.bodyViewportElement.addEventListener('scroll', self.onBodyScroll);
        },
        /*
         * @private
         */
        resetScroll: function() {
            this.bodyViewportElement.scrollTop = 0; // set scrollTop back to top of table
        },
        /*
         * @private
         */
        updateTable: function(dataGridChgInfo, currentDataGridIds) {
            var self = this,
                linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
                usedBufferIndices = [];
            // precondition: pageBuf.size is not changed and !== 0
            if ((self.pageBuf.rows.length === 0) || (self.pageBuf.bufferSize < currentDataGridIds.length)) {
                console.error("[ComplexTable2]", "updateTable, table is empty or buffer size has been changed");
                return;
            }
            //self.pageBuf.offset >= self.pageBuf.bufferOffset
            var indexOffset = self.pageBuf.offset - self.pageBuf.bufferOffset;
            currentDataGridIds.forEach(function(dgId, idx) {
                var currentIndex = idx - indexOffset;
                if ((currentIndex < 0) || (currentIndex >= (linesPerPage * 3))) {
                    return;
                }
                var curPage = Math.floor(currentIndex / linesPerPage),
                    bufIdx = (self.vars.pageOrder[curPage] * linesPerPage) + (currentIndex % linesPerPage),
                    curRow = self.pageBuf.rows[bufIdx];

                if (curRow) {
                    usedBufferIndices.push(bufIdx);
                    if (curRow.rowId !== dgId || !curRow.visible) {
                        self.writeRow(bufIdx, currentDataGridIds, idx);
                    }
                } else {
                    console.error("[ComplexTable2]", "invalid buffer index:", bufIdx);
                }
            });

            self.pageBuf.rows.forEach(function(r, idx) {
                if (usedBufferIndices.indexOf(idx) === -1) {
                    if (r.visible) {
                        r.visible = false;
                        r.trElem.style.visibility = "hidden";
                    }
                } else if (!r.visible) {
                    r.visible = true;
                    r.trElem.style.visibility = "";
                }
            });
        },
        /*
         * @private
         */
        writeRow: function(rId, currentDataGridIds, bufRow) {
            var rowId = 0,
                item = "",
                cfgCol = null;

            if (this.pageBuf.rows[rId]) {
                rowId = this.pageBuf.rows[rId].rowId = currentDataGridIds[bufRow];

                for (var col = 0; col < this.pageBuf.nofCols; col++) {
                    cfgCol = this.cConfig.resp["column-org"][col];
                    var ctrls = this.pageBuf.rows[rId].cols[col].ctrls;
                    for (var ctrlIdx = 0; ctrlIdx < ctrls.length; ctrlIdx++) {
                        item = this.makeItem(rowId, cfgCol.fields[ctrlIdx].dgCol);

                        var ia = shmi.requires("visuals.tools.item-adapter"),
                            columnAdapter = this.getColumnAdapter(cfgCol.fields[ctrlIdx].fieldName);
                        if (shmi.visuals.session.ItemManager.getItem(item) !== null) {
                            if (columnAdapter === null) {
                                ia.unsetAdapter(item);
                            } else {
                                ia.setAdapter(item, columnAdapter);
                            }
                        }
                        ctrls[ctrlIdx].setItem(item);
                    }
                }
            }
        },
        /**
         * @private
         */
        updateMetricsData: function(scrollTop) {
            const self = this;

            if (self.pageBuf.size > 0) {
                self.metricsData.lineHeightPx = self.tbodyElement[0].getBoundingClientRect().height / self.tbodyElement[0].rows.length;
                if (self.metricsData.lineHeightPx < 1) { //all columns hidden
                    if (self.metricsData.last.isValid) {
                        self.metricsData = shmi.cloneObject(self.metricsData.last);
                    } else {
                        self.metricsData.lineHeightPx = self.c.DEFAULT_LINE_HEIGHT;
                    }
                }
            } else if (self.metricsData.last.isValid) { // empty table
                self.metricsData = shmi.cloneObject(self.metricsData.last);
            } else {
                self.metricsData.lineHeightPx = self.c.DEFAULT_LINE_HEIGHT;
            }

            self.tableElement.style.height = (self.pageBuf.nofTotalRows * self.metricsData.lineHeightPx) + "px";
            self.tableElement.style.overflow = "hidden";
            self.metricsData.viewportHeight = self.bodyViewportElement.getBoundingClientRect().height;
            self.metricsData.viewportWidth = self.bodyViewportElement.getBoundingClientRect().width;

            const minBufRows = Math.ceil(self.metricsData.viewportHeight / self.metricsData.lineHeightPx) * 3;
            if (self.cConfig["nof-buffered-rows"] < minBufRows) {
                self.cConfig["nof-buffered-rows"] = minBufRows;
                console.info("[ComplexTable2]", "buffer rows configured too low, corrected to:", minBufRows);
                self.cleanupTabContent();
                self.reinitTable(this.dgSubscription.changeInfo, self.dgMan.getCurrentIDs(self.cConfig.table, self.dgSubscription.id));
            }

            const nofRows = self.cConfig["nof-buffered-rows"];
            self.pageBuf.nofTotalPages = self.pageBuf.nofTotalRows / (nofRows / 3);
            self.metricsData.isValid = true;

            if (typeof scrollTop === "number") {
                reinitPages(self, scrollTop);
            } else {
                self.vars.scrollTop = 0;
                reinitPages(self, 0);
            }
        },
        /**
         * @private
         */
        disableEvents: function() {
            if (this.header.selAllCBox) {
                this.header.selAllCBox.setEventsEnabled(false);
            }
        },
        /**
         * @private
         */
        enableEvents: function() {
            if (this.header.selAllCBox) {
                this.header.selAllCBox.setEventsEnabled(true);
            }
        },
        /**
         * @private
         */
        // $note: col 0..nofCols-1
        createHeaderCtrls: function(tabCellElem, col) {
            var self = this,
                hCtrls = [];
            shmi.addClass(tabCellElem, this.uiCssCl.thCont);
            var cfgCol = this.cConfig.resp["column-org"][col],
                nofFields = cfgCol.fields.length;
            for (var headIdx = 0; headIdx < nofFields; headIdx++) {
                var hCtrlElem = document.createElement("div"),
                    icon = cfgCol.fields[headIdx].icon,
                    hCaption = cfgCol.fields[headIdx].header || " ";
                if (hCaption) {
                    var lhCaption = shmi.localize(hCaption);
                    if (headIdx > 0) {
                        lhCaption = " / " + lhCaption;
                    }
                    var hCtrl = new shmi.visuals.controls.ComplexTable2.FieldHeader(
                        hCtrlElem,
                        cfgCol.fields[headIdx].dgCol, // id
                        cfgCol.fields[headIdx].isSortable,
                        this.c.NO_SORT,
                        lhCaption,
                        self,
                        cfgCol.fields[headIdx].fieldName, icon);
                    hCtrl.setChangeCallback(function(id, state) {
                        if (!self.locked) {
                            self.sortHeaderChanged(id, state);
                        }
                    });
                    hCtrls.push(hCtrl);
                    tabCellElem.appendChild(hCtrlElem);
                }
            }

            return hCtrls;
        },
        /**
         * @private
         */
        // $note: col 0..nofCols-1
        createColTxtFilterCtrls: function(tabCellElem, col) {
            var cfCtrls = [],
                cfContainerElem = document.createElement("div"),
                cfLineElem = document.createElement("div");
            cfContainerElem.appendChild(cfLineElem);
            tabCellElem.appendChild(cfContainerElem);

            shmi.addClass(tabCellElem, this.uiCssCl.thCont);
            shmi.addClass(cfContainerElem, this.uiCssCl.colTxtFilterCont);
            shmi.addClass(cfLineElem, this.uiCssCl.colTxtFilterLineCont);

            var iName = this.colTxtFilters.items[col].name,
                cfg = this.getChildCtrlConfig(this.cConfig.resp["text-filter-input-field-config"]);
            if (!cfg) {
                cfg = {
                    "label": this.config["filter-label"],
                    "placeholder": this.config["filter-placeholder"],
                    "class-name": "iqel-filter-input",
                    "unit-text": null,
                    "template": this.config["filter-template"],
                    "show-icon": !!this.config["filter-icon-src"],
                    "icon-src": this.config["filter-icon-src"],
                    "inline-svg-icon": true
                };
            }
            cfg.item = iName;
            var fInpFld = shmi.createControl("iq-input-field", cfLineElem, cfg, "div");
            cfCtrls.push(fInpFld);

            const elClearFilter = document.createElement("div");
            elClearFilter.classList.add("iqel-clear-filter");
            shmi.requires("visuals.tools.icons").applyIcon(elClearFilter, this.config["clear-icon-src"], null, true);
            cfLineElem.appendChild(elClearFilter);

            const clearFilterHandler = () => {
                if (this.isActive() && !this.isLocked()) {
                    this.colTxtFilterReset(col);
                }
            };

            elClearFilter.addEventListener('click', clearFilterHandler);
            elClearFilter.addEventListener('touchstart', clearFilterHandler);

            return cfCtrls;
        },
        /**
         * @private
         */
        createSelCBox: function(tabCellElem, id, is3state, ct2ref, iconPathChecked, iconPathUndefined) {
            return new shmi.visuals.controls.ComplexTable2.CheckBox(tabCellElem, id, is3state, this.c.CB_UNCHK, ct2ref, iconPathChecked, iconPathUndefined);
        },
        /**
         * @private
         */
        createSelRowCtrl: function(trElem, id, ct2ref) {
            return new shmi.visuals.controls.ComplexTable2.RowSelector(trElem, id, this.c.CB_UNCHK, ct2ref, {
                toggleSelection: this.cConfig.resp["toggle-selection"],
                doubleClickEvents: this.cConfig.resp["double-click-events"]
            });
        },
        /**
         * @private
         */
        createColGroup: function(nofCols, isMultiSelectMode) {
            var colGroupElem = document.createElement("colgroup");
            var columns = [];
            if (isMultiSelectMode && this.config["show-select-boxes"]) {
                columns.push(document.createElement("col"));
                columns[columns.length - 1].setAttribute("class", this.uiCssCl.colCb);
            }
            for (var i = 0; i < nofCols; i++) {
                var cfgCol = this.cConfig.resp["column-org"][i];
                columns.push(document.createElement("col"));
                columns[columns.length - 1].setAttribute("class", this.uiCssCl.colPfx + (i + 1));
                if (cfgCol.fields[0]["column-width"]) {
                    columns[columns.length - 1].style.width = cfgCol.fields[0]["column-width"];
                }
            }
            columns.forEach(function(c) {
                colGroupElem.appendChild(c);
            });
            return colGroupElem;
        },
        /**
         * @private
         */
        getTableWidth: function() {
            var width = 0;
            if (this.tcElement) {
                width = this.tcElement.offsetWidth;
            } else {
                var tcElem = shmi.getUiElement(this.uiElName.tabContainer, this.element);
                if (!tcElem) {
                    shmi.log("[ComplexTable2] no " + this.uiElName.tabContainer + " element provided (required)", 3);
                } else {
                    width = tcElem.offsetWidth;
                }
            }
            return width;
        },
        /**
         * @private
         */
        getTableVpWidth: function() {
            return this.bodyViewportElement.offsetWidth;
        },
        /**
         * @private
         */
        isTabSizeChanged: function() {
            var changed = false;
            if ((this.getTableVpWidth() !== this.lastTableVpWidth) || (this.element.offsetHeight !== this.lastTableVpHeight)) {
                changed = true;
                this.lastTableVpWidth = this.getTableVpWidth();
                this.lastTableVpHeight = this.element.offsetHeight;
            }
            return changed;
        },
        /**
         * @private
         */
        isLayoutChanged: function() {
            var changed = false;
            if (this.cConfig.resp["layout-id"] !== this.lastLayoutId) {
                changed = true;
                this.lastLayoutId = this.cConfig.resp["layout-id"];
            }
            return changed;
        },
        /**
         * @private
         */
        makeItem: function(dgRowId, dgCol) {
            return this.dgSubscription.prefix + dgRowId + ":" + dgCol;
        },
        /**
         * @private
         */
        searchRowId: function(rowId) {
            var bufRowFound = -1;
            for (var bufRow = 0; bufRow < this.pageBuf.viewportSize; bufRow++) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].rowId === rowId) {
                    bufRowFound = bufRow;
                    break;
                }
            }
            return bufRowFound;
        },
        /**
         * @private
         */
        detectTouchDevice: function() {
            return !!("ontouchstart" in document.documentElement);
        },
        onLock: function() {
            var self = this;
            shmi.addClass(self.element, "locked");
            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable && self.ctrlPanelData.qSearchInput) {
                self.ctrlPanelData.qSearchInput.lock();
            }
        },
        onUnlock: function() {
            var self = this;
            shmi.removeClass(self.element, "locked");
            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable && self.ctrlPanelData.qSearchInput) {
                self.ctrlPanelData.qSearchInput.unlock();
            }
        }
    };

    shmi.extend(shmi.visuals.controls.ComplexTable2, shmi.visuals.core.BaseControl);
    shmi.registerControlType("complex-table2", shmi.visuals.controls.ComplexTable2, true);

    /*-- CT2 helper classes --*/

    /**
     * Creates a new ComplexTable2.CheckBox control.
     *
     * @param {HTMLElement} element <div></div>
     * @param {number} id bufRow 0..n, -1 for summary CB
     * @param {boolean} is3State
     * @param {number} initialState shmi.visuals.controls.ComplexTable2.c.CB_??
     * @param {object} ct2ref ct2 reference
     *
     * @constructor
     */
    shmi.visuals.controls.ComplexTable2.CheckBox = function(element, id, is3State, initialState, ct2ref, iconPathChecked, iconPathUndefined) {
        this.ct2c = shmi.visuals.controls.ComplexTable2.c; // short cut
        this.css = shmi.visuals.controls.ComplexTable2.CheckBox.uiCssCl; // short cut
        this.element = element;
        this.id = id;
        this.is3tate = is3State;
        this.state = initialState;
        this.chgCallb = null;
        this.cbMouseListener = null;
        this.cbTouchListener = null;

        shmi.addClass(this.element, shmi.visuals.controls.ComplexTable2.uiCssCl.selCb);

        const elChecked = document.createElement("div");
        elChecked.classList.add("iqel-checkmark");
        shmi.requires("visuals.tools.icons").applyIcon(elChecked, iconPathChecked, null, true);
        const elUndefined = document.createElement("div");
        elUndefined.classList.add("iqel-undefined");
        shmi.requires("visuals.tools.icons").applyIcon(elUndefined, iconPathUndefined, null, true);
        this.element.appendChild(elChecked);
        this.element.appendChild(elUndefined);

        var cbEvents = {};
        cbEvents.onPress = function() {
            if (!ct2ref.locked) {
                shmi.addClass(this.element, this.css.sel);
            }
        }.bind(this);
        cbEvents.onRelease = function() {
            if (!ct2ref.locked) {
                shmi.removeClass(this.element, this.css.sel);
            }
        }.bind(this);
        cbEvents.onClick = function() {
            // process event
            if (!ct2ref.locked) {
                if (this.is3tate) {
                    switch (this.state) {
                    case this.ct2c.CB_UNCHK:
                        this.state = this.ct2c.CB_CHK;
                        break;
                    case this.ct2c.CB_CHK:
                        this.state = this.ct2c.CB_UNDEF;
                        break;
                    default:
                        this.state = this.ct2c.CB_UNCHK;
                        break;
                    }
                } else {
                    this.state = (this.state === this.ct2c.CB_UNCHK) ? this.ct2c.CB_CHK : this.ct2c.CB_UNCHK;
                }
                this.updateState();
                this.fireCbChg();
            }
        }.bind(this);
        this.cbMouseListener = new shmi.visuals.io.MouseListener(this.element, cbEvents);
        this.cbTouchListener = new shmi.visuals.io.TouchListener(this.element, cbEvents);

        this.updateState();
        this.setEventsEnabled(true);
    };
    shmi.visuals.controls.ComplexTable2.CheckBox.prototype = {
        setChangeCallback: function(listener) { // null: no event listening
            this.chgCallb = listener;
        },
        setEventsEnabled: function(enabled) {
            if (enabled) {
                this.cbMouseListener.enable();
                this.cbTouchListener.enable();
            } else {
                this.cbMouseListener.disable();
                this.cbTouchListener.disable();
            }
        },
        setState: function(state) {
            this.state = state;
            this.updateState();
        },
        getState: function() {
            return this.state;
        },
        /**
         * @private
         */
        updateState: function() {
            shmi.removeClass(this.element, this.css.ck);
            shmi.removeClass(this.element, this.css.unck);
            shmi.removeClass(this.element, this.css.undef);
            switch (this.state) {
            case this.ct2c.CB_UNCHK:
                shmi.addClass(this.element, this.css.unck);
                break;
            case this.ct2c.CB_CHK:
                shmi.addClass(this.element, this.css.ck);
                break;
            default:
                shmi.addClass(this.element, this.css.undef);
                break;
            }
        },
        /**
         * @private
         */
        fireCbChg: function() {
            if (this.chgCallb) {
                this.chgCallb(this.id, this.state);
            }
        }
    };

    /**
     * Creates a new ComplexTable2.RowSelector control.
     *
     * @param {HTMLElement} element <tr>...</tr>
     * @param {number} id bufRow 0..n
     * @param {number} initialState shmi.visuals.controls.ComplexTable2.c.CB_??
     * @param {object} ct2ref ct2 reference
     * @param {object} [options] options
     *
     * @constructor
     */
    shmi.visuals.controls.ComplexTable2.RowSelector = function(element, id, initialState, ct2ref, options) {
        var self = this;
        self.ct2c = shmi.visuals.controls.ComplexTable2.c; // short cut
        self.css = shmi.visuals.controls.ComplexTable2.RowSelector.uiCssCl; // short cut
        self.element = element;
        self.id = id;
        self.state = initialState;
        self.chgCallb = null;
        self.rsMouseListener = null;
        self.rsTouchListener = null;
        self.toggleSelection = options?.toggleSelection ?? true;

        var rsEvents = {};
        rsEvents.onPress = function() {
            if (!ct2ref.locked) {
                shmi.addClass(self.element, self.css.sel);
            }
        };
        rsEvents.onRelease = function() {
            if (!ct2ref.locked) {
                shmi.removeClass(self.element, self.css.sel);
            }
        };
        rsEvents.onClick = function(x, y, evt) {
            // process event
            if (!ct2ref.locked) {
                self.shiftPressed = evt.shiftKey;
                self.ctrlPressed = evt.ctrlKey;
                self.dblClicked = false;
                self.state = (!self.toggleSelection || self.state === self.ct2c.CB_UNCHK) ? self.ct2c.CB_CHK : self.ct2c.CB_UNCHK;
                self.updateState();
                self.fireRsChg();
            }
        };

        if (options?.doubleClickEvents ?? true) {
            rsEvents.onDoubleClick = function(x, y, evt) {
                // process event
                if (!ct2ref.locked) {
                    self.shiftPressed = evt.shiftKey;
                    self.ctrlPressed = evt.ctrlKey;
                    self.dblClicked = true;
                    self.state = self.ct2c.CB_CHK;
                    self.updateState();
                    self.fireRsChg();
                }
            };
        }

        self.rsMouseListener = new shmi.visuals.io.MouseListener(self.element, rsEvents);
        self.rsTouchListener = new shmi.visuals.io.TouchListener(self.element, rsEvents);

        self.updateState();
        self.setEventsEnabled(true);
    };
    shmi.visuals.controls.ComplexTable2.RowSelector.prototype = {
        setChangeCallback: function(listener) { // null: no event listening
            this.chgCallb = listener;
        },
        setEventsEnabled: function(enabled) {
            if (enabled) {
                this.rsMouseListener.enable();
                this.rsTouchListener.enable();
            } else {
                this.rsMouseListener.disable();
                this.rsTouchListener.disable();
            }
        },
        setState: function(state) {
            this.state = state;
            this.updateState();
        },
        getState: function() {
            return this.state;
        },
        /**
         * @private
         */
        updateState: function() {
            shmi.removeClass(this.element, this.css.ck);
            shmi.removeClass(this.element, this.css.unck);
            if (this.state === this.ct2c.CB_CHK) {
                shmi.addClass(this.element, this.css.ck);
            } else {
                shmi.addClass(this.element, this.css.unck);
            }
        },
        /**
         * @private
         */
        fireRsChg: function() {
            if (this.chgCallb) {
                this.chgCallb(this.id, this.state, this.shiftPressed, this.ctrlPressed, this.dblClicked);
            }
        }
    };

    /**
     * Creates a new ComplexTable2.FieldHeader control.
     *
     * @param {HTMLElement} element <div></div>
     * @param {number} id
     * @param {boolean} isSortable
     * @param {number} initialState shmi.visuals.controls.ComplexTable2.c.SORT_??
     * @param {string} caption (already localized !!)
     *
     * @constructor
     */
    shmi.visuals.controls.ComplexTable2.FieldHeader = function(element, id, isSortable, initialState, caption, ct2Inst, fldName, icon) {
        this.ct2c = shmi.visuals.controls.ComplexTable2.c; // short cut
        this.css = shmi.visuals.controls.ComplexTable2.FieldHeader.uiCssCl; // short cut
        this.ct2Inst = ct2Inst;
        this.element = element;
        this.id = id;
        this.isSortable = isSortable;
        this.state = initialState;
        this.caption = caption;
        this.chgCallb = null;
        this.cbMouseListener = null;
        this.cbTouchListener = null;
        this.fieldName = (fldName !== undefined) ? fldName : null;
        this.icon = (icon === undefined) ? null : icon;

        const elText = document.createElement("div"),
            elSort = document.createElement("div"),
            elSortUp = document.createElement("div"),
            elSortDown = document.createElement("div");

        elText.classList.add("iqel-header-text");
        elSort.classList.add("iqel-header-sort");
        elSortUp.classList.add("iqel-header-sort-up");
        elSortDown.classList.add("iqel-header-sort-down");
        elSort.appendChild(elSortUp);
        elSort.appendChild(elSortDown);

        shmi.requires("visuals.tools.icons").applyIcon(elSortUp, ct2Inst.config["sort-up-icon-src"], null, true);
        shmi.requires("visuals.tools.icons").applyIcon(elSortDown, ct2Inst.config["sort-down-icon-src"], null, true);

        shmi.addClass(this.element, shmi.visuals.controls.ComplexTable2.uiCssCl.fldHeader);
        //element.textContent = this.caption;
        if (this.ct2Inst.headerMode === "ICON") {
            if (this.icon !== null) {
                shmi.addClass(elText, this.icon);
            }
        } else {
            elText.textContent = this.caption;
        }

        this.element.appendChild(elText);
        this.element.appendChild(elSort);

        if (this.isSortable) {
            shmi.addClass(this.element, this.css.sortable);

            var cbEvents = {};
            cbEvents.onPress = function() {
                if (!this.ct2Inst.locked) {
                    shmi.addClass(this.element, this.css.sel);
                }
            }.bind(this);
            cbEvents.onRelease = function() {
                if (!this.ct2Inst.locked) {
                    shmi.removeClass(this.element, this.css.sel);
                }
            }.bind(this);
            cbEvents.onClick = function() {
                if (!this.ct2Inst.locked) {
                    // process event
                    switch (this.state) {
                    case this.ct2c.SORT_UP:
                        this.state = this.ct2c.SORT_DOWN;
                        break;
                    case this.ct2c.SORT_DOWN:
                        this.state = this.ct2c.NO_SORT;
                        break;
                    default:
                        this.state = this.ct2c.SORT_UP; // in all other cases, it's impossible to reach NO_SORT by event
                        break;
                    }
                    this.updateState();
                    this.fireSortChg();
                }
            }.bind(this);
            this.cbMouseListener = new shmi.visuals.io.MouseListener(this.element, cbEvents);
            this.cbTouchListener = new shmi.visuals.io.TouchListener(this.element, cbEvents);
        }

        this.updateState();
        this.setEventsEnabled(true);
    };
    shmi.visuals.controls.ComplexTable2.FieldHeader.prototype = {
        setChangeCallback: function(listener) { // null: no event listening
            this.chgCallb = listener;
        },
        setEventsEnabled: function(enabled) {
            if (this.cbMouseListener) {
                if (enabled) {
                    this.cbMouseListener.enable();
                    this.cbTouchListener.enable();
                } else {
                    this.cbMouseListener.disable();
                    this.cbTouchListener.disable();
                }
            }
        },
        setState: function(state) {
            this.state = state;
            this.updateState();
        },
        getState: function() {
            return this.state;
        },
        /**
         * @private
         */
        updateState: function() {
            if (this.isSortable) {
                shmi.removeClass(this.element, this.css.noSort);
                shmi.removeClass(this.element, this.css.sortUp);
                shmi.removeClass(this.element, this.css.sortDn);
                switch (this.state) {
                case this.ct2c.SORT_UP:
                    shmi.addClass(this.element, this.css.sortUp);
                    break;
                case this.ct2c.SORT_DOWN:
                    shmi.addClass(this.element, this.css.sortDn);
                    break;
                default: // NO_SORT
                    shmi.addClass(this.element, this.css.noSort);
                    break;
                }
            } else {
                shmi.addClass(this.element, this.css.noSort);
            }
        },
        /**
         * @private
         */
        fireSortChg: function() {
            if (this.chgCallb) {
                this.chgCallb(this.id, this.state);
            }
        }
    };

    shmi.visuals.controls.ComplexTable2.instIDs = [];

    shmi.visuals.controls.ComplexTable2.uiElName = {};
    (function() {
        /* The following ui element names are used in CT2's JS code. Change it here if necessary.
         All other ui element names may be changed in the CT2 template(s).
         */
        var __ct2sc = shmi.visuals.controls.ComplexTable2.uiElName;
        __ct2sc.tabContContainer = "iqel-table-content-container";
        __ct2sc.tabHeadContainer = "iqel-table-header-container";
        __ct2sc.tabContainer = "iqel-table-container";
        __ct2sc.tbodyContainer = "iqel-table-body-container";
        __ct2sc.thead = "iqel-table-header";
        __ct2sc.table = "iqel-table";
        __ct2sc.txtFiltersHead = "iqel-table-filters";
        __ct2sc.tbody = "iqel-table-body";
        __ct2sc.label = "iqel-label";
        __ct2sc.ctrlPanel = "iqel-ctrl-panel";
        __ct2sc.footer = "iqel-table-footer";

        shmi.visuals.controls.ComplexTable2.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.uiCssCl;
        /* The following ui element class names are used in CT2's JS code. Change it here if necessary.
         All other ui element class names be changed in the CT2 template(s).
         */
        // (static) styles for dyn. created elements
        // !!! keep them short !!! because they are used in each table row / cell
        __ct2sc.colCb = "iqel-col-checkbox";
        __ct2sc.colPfx = "iqel-col"; // prefix, the classes are iqel-col1, iqel-col2, ..., iqel-col<n>
        __ct2sc.thCont = "iqel-th-content";
        __ct2sc.tdCont = "iqel-td-content";
        __ct2sc.selCb = "iqel-selection-checkbox";
        __ct2sc.evenRow = "iqel-even-row";
        __ct2sc.fldHeader = "iqel-field-header";
        __ct2sc.colTxtFilterCont = "iqel-filter";
        __ct2sc.colTxtFilterLineCont = "iqel-line";

        // dynamically switched styles
        __ct2sc.multiselect = "multi-select";
        __ct2sc.rowDisplVisible = "row-display-visible";
        __ct2sc.rowDisplInvisible = "row-display-invisible";
        __ct2sc.emptyTab = "empty-table";
        __ct2sc.ctrlPanelMin = "ctrl-panel-hidden";
        __ct2sc.ctrlPanelNormal = "ctrl-panel-normal";
        __ct2sc.activeRow = "iqel-active-row";
        __ct2sc.touchDev = "touch";
        __ct2sc.noTouchDev = "notouch";

        shmi.visuals.controls.ComplexTable2.c = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.c;
        //--
        __ct2sc.MIN_BUF_SIZE = 150;
        __ct2sc.CONST_LINE_HEIGHT = false;
        __ct2sc.DEBOUNCE_TO_RESIZE = 150;
        __ct2sc.DEFAULT_LINE_HEIGHT = 40;
        __ct2sc.MULTI_SELECT_MODE = "3-state-clear"; // "3-state", "2-state" (NYI)
        __ct2sc.SCROLL_EVT_TYPE_NONE = -1;
        __ct2sc.SCROLL_EVT_TYPE_TOUCH = 11;
        __ct2sc.SCROLL_EVT_TYPE_RELEASE = 12;
        __ct2sc.SCROLL_EVT_TYPE_SB_BTN_UP = 101;
        __ct2sc.SCROLL_EVT_TYPE_SB_BTN_DOWN = 102;
        __ct2sc.SCROLL_EVT_TYPE_SB_SLIDER_CHG = 103;
        __ct2sc.SCROLL_EVT_TYPE_SB_LARGE_CHG = 104;
        __ct2sc.SCROLL_EVT_TYPE_V_DRAG_UP = 201;
        __ct2sc.SCROLL_EVT_TYPE_V_DRAG_DOWN = 202;
        __ct2sc.SCROLL_EVT_TYPE_V_SWIPE_UP = 301;
        __ct2sc.SCROLL_EVT_TYPE_V_SWIPE_DOWN = 302;
        __ct2sc.SCROLL_EVT_TYPE_H_SWIPE_LEFT = 401;
        __ct2sc.SCROLL_EVT_TYPE_H_SWIPE_RIGHT = 402;
        __ct2sc.SCROLL_EVT_TYPE_RESIZE = 501;
        __ct2sc.SCROLL_EVT_TYPE_RESIZE_LAYOUT_CHG = 502;
        //--
        __ct2sc.SELECT_TYPE_NOTHING = -1;
        __ct2sc.SELECT_TYPE_ALL = 1;
        __ct2sc.SELECT_TYPE_SEL_ROWS = 2;
        //--
        __ct2sc.CB_UNCHK = -1;
        __ct2sc.CB_CHK = 1;
        __ct2sc.CB_UNDEF = 2;
        __ct2sc.NO_SORT = -1;
        __ct2sc.SORT_UP = 1;
        __ct2sc.SORT_DOWN = 2;
        //--

        shmi.visuals.controls.ComplexTable2.CheckBox.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.CheckBox.uiCssCl;
        // dyn. created checkbox classes
        // !!! keep them short !!! because they are used in each table row
        __ct2sc.sel = "pressed";
        __ct2sc.ck = "checked";
        __ct2sc.unck = "unchecked";
        __ct2sc.undef = "undefined";

        shmi.visuals.controls.ComplexTable2.RowSelector.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.RowSelector.uiCssCl;
        // dyn. created row selector classes
        // !!! keep them short !!! because they are used in each table row
        __ct2sc.sel = "pressed";
        __ct2sc.ck = "row-selected";
        __ct2sc.unck = "row-unselected";

        shmi.visuals.controls.ComplexTable2.FieldHeader.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.FieldHeader.uiCssCl;
        // dyn. created (sortable) header field classes
        __ct2sc.sel = "pressed";
        __ct2sc.sortUp = "header-sort-up";
        __ct2sc.sortDn = "header-sort-down";
        __ct2sc.noSort = "header-not-sortable";
        __ct2sc.sortable = "header-sortable";
    })();
}());

(function() {
    'use strict';

    const modifiers = {
        MODE: {
            IQ_FLEX: "iq-flex-layout"
        },
        ALIGN: {
            PRIMARY: {
                START: "flex-primary-align-start",
                CENTER: "flex-primary-align-center",
                END: "flex-primary-align-end",
                SPACE_BETWEEN: "flex-primary-align-space-between",
                SPACE_AROUND: "flex-primary-align-space-around",
                SPACE_EVENLY: "flex-primary-align-space-evenly"
            },
            SECONDARY: {
                START: "flex-secondary-align-start",
                CENTER: "flex-secondary-align-center",
                END: "flex-secondary-align-end",
                STRETCH: "flex-secondary-align-stretch"
            },
            LINE: {
                START: "flex-line-align-start",
                CENTER: "flex-line-align-center",
                END: "flex-line-align-end",
                STRETCH: "flex-line-align-stretch",
                SPACE_BETWEEN: "flex-line-align-space-between",
                SPACE_AROUND: "flex-line-align-space-around"
            }
        },
        FLEX: {
            DISTRIBUTE: "distribute-evenly",
            WRAP: "flex-wrap",
            ALL: "flex-all",
            COLUMN: "column-orientation",
            NONE: "flex-none"
        }
    };

    /**
     * Maps config options to modifiers.
     */
    const modifierMap = {
        "flex-orientation": [
            { value: "column", modifier: modifiers.FLEX.COLUMN }
        ],
        "flex-distribute": [
            { value: true, modifier: modifiers.FLEX.DISTRIBUTE }
        ],
        "flex-all": [
            { value: true, modifier: modifiers.FLEX.ALL }
        ],
        "flex-none": [
            { value: true, modifier: modifiers.FLEX.NONE }
        ],
        "flex-wrap": [
            { value: true, modifier: modifiers.FLEX.WRAP }
        ],
        "flex-primary-align": [
            { value: "start", modifier: modifiers.ALIGN.PRIMARY.START },
            { value: "center", modifier: modifiers.ALIGN.PRIMARY.CENTER },
            { value: "end", modifier: modifiers.ALIGN.PRIMARY.END },
            { value: "space-between", modifier: modifiers.ALIGN.PRIMARY.SPACE_BETWEEN },
            { value: "space-around", modifier: modifiers.ALIGN.PRIMARY.SPACE_AROUND },
            { value: "space-evenly", modifier: modifiers.ALIGN.PRIMARY.SPACE_EVENLY }
        ],
        "flex-secondary-align": [
            { value: "start", modifier: modifiers.ALIGN.SECONDARY.START },
            { value: "center", modifier: modifiers.ALIGN.SECONDARY.CENTER },
            { value: "end", modifier: modifiers.ALIGN.SECONDARY.END },
            { value: "stretch", modifier: modifiers.ALIGN.SECONDARY.STRETCH }
        ],
        "flex-line-align": [
            { value: "start", modifier: modifiers.ALIGN.LINE.START },
            { value: "center", modifier: modifiers.ALIGN.LINE.CENTER },
            { value: "end", modifier: modifiers.ALIGN.LINE.END },
            { value: "stretch", modifier: modifiers.ALIGN.LINE.STRETCH },
            { value: "space-between", modifier: modifiers.ALIGN.LINE.SPACE_BETWEEN },
            { value: "space-around", modifier: modifiers.ALIGN.LINE.SPACE_AROUND }
        ]
    };

    const containerVariants = {
        iqflex: {
            baseModifier: modifiers.MODE.IQ_FLEX,
            modifierSettings: ["flex-orientation", "flex-wrap", "flex-primary-align", "flex-secondary-align", "flex-line-align", "flex-none"]
        }
    };

    function clearAllModifiers(element) {
        const allModifiers = [
            ...Object.values(modifiers.MODE),
            ...Object.values(modifiers.FLEX)
        ].filter((val) => val !== null);

        element.classList.remove(...allModifiers);
    }

    function applyModifiers(element, config) {
        const variantSettings = containerVariants[config.type];
        if (!variantSettings) {
            console.error("[Container]", "unknown layout type:", config.type);
            return;
        }

        // Add base modifier if required
        if (variantSettings.baseModifier) {
            shmi.addClass(element, variantSettings.baseModifier);
        }

        // Add conditional modifiers
        variantSettings.modifierSettings.forEach((modifierKey) => {
            const modifierConditions = modifierMap[modifierKey];
            if (!modifierConditions || !Array.isArray(modifierConditions)) {
                console.error("[Container]", "missing modifier info:", modifierKey);
                return;
            }

            // Get modifier for the config value.
            const modifierInfo = modifierConditions.find(({ value }) => config[modifierKey] === value);
            if (modifierInfo) {
                shmi.addClass(element, modifierInfo.modifier);
            }
        });
    }

    function getMarginCompensator(self) {
        let marginCompensator = self.element.querySelector("div.margin-compensator"),
            oldHtml = null;

        if (!marginCompensator || (marginCompensator.parentNode !== self.element)) {
            marginCompensator = document.createElement("DIV");
            shmi.addClass(marginCompensator, "margin-compensator");
            oldHtml = self.element.innerHTML;
            self.element.innerHTML = "";
            marginCompensator.innerHTML = oldHtml;
            self.element.appendChild(marginCompensator);
        }

        return marginCompensator;
    }

    shmi.visuals.controls.Container = function(element, config) {
        var self = this;
        self.element = element;
        self.config = config || {};

        self.parseAttributes();

        shmi.def(self.config, 'class-name', "iq-container");
        shmi.def(self.config, 'name', null);
        shmi.def(self.config, 'type', "iqflex"); //"stacked", "float", "inline", "flex", "iqflex"
        shmi.def(self.config, 'flex-orientation', "column"); //"row", "column"
        shmi.def(self.config, 'flex-distribute', false);
        shmi.def(self.config, 'flex-all', false);
        shmi.def(self.config, 'flex-none', false);
        shmi.def(self.config, 'flex-wrap', false);
        shmi.def(self.config, 'flex-primary-align', "start");
        shmi.def(self.config, 'flex-secondary-align', "stretch");
        shmi.def(self.config, 'flex-line-align', "start");

        self.controls = [];
        self.marginCompensator = null;

        self.startup();
    };

    shmi.visuals.controls.Container.prototype = {
        uiType: "container",
        isContainer: true,
        getClassName: function() {
            return "Container";
        },
        /**
         * Parses child controls when control is registered in layout
         *
         * @param {function} onDone function to call on completion
         * @returns {undefined}
         */
        onRegister: function(onDone) {
            var self = this;

            //clear active css-modifiers, in case of reconfiguration
            clearAllModifiers(self.element);
            applyModifiers(self.element, self.config);
            self.marginCompensator = getMarginCompensator(self);
            self.parseChildren(self.marginCompensator, onDone);
        },
        onEnable: function() {
            const self = this;
            self.controls.forEach((c) => c.enable());
        },
        onDisable: function() {
            const self = this;
            self.controls.forEach((c) => c.disable());
        },
        onLock: function() {

        },
        onUnlock: function() {

        },
        onAddControl: function(options, callback) {
            const self = this,
                cm = shmi.requires("visuals.tools.controller"),
                controls = [];

            let initToken = null;

            if (!self.initialized) {
                throw new Error("Control not initialized yet.");
            }

            if (!Array.isArray(options)) {
                options = [options];
            }
            options.forEach(function(option) {
                const control = shmi.createControl(option.ui, self.marginCompensator, option.config, "DIV", null, false);
                if (control !== null) {
                    if (option.style && typeof option.style === "object") {
                        Object.assign(control.element.style, option.style);
                    }
                    controls.push(control);
                }
            });
            initToken = shmi.waitOnInit(controls, function() {
                if (initToken) {
                    const idx = self._init_.tokens.indexOf(initToken);
                    if (idx !== -1) {
                        self._init_.tokens.splice(idx, 1);
                    }
                    initToken.unlisten();
                    initToken = null;
                }

                const promises = options.map(function(opt, jdx) {
                    const control = controls[jdx];

                    if (opt.controller) {
                        cm.create(opt.controller.name, opt.controller, control.getName());
                    }

                    if (self.isActive()) {
                        control.enable();
                    }

                    if (!Array.isArray(opt.children) || !opt.children.length) {
                        return Promise.resolve();
                    }

                    return new Promise((resolve) => {
                        control.addControl(opt.children, (childErr) => resolve(childErr));
                    });
                });

                Promise.all(promises).then((result) => {
                    const errors = result.filter((r) => !!r);
                    callback(errors.length ? errors[0] : null, controls);
                });
            });
            if (initToken) {
                self._init_.tokens.push(initToken);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.Container, shmi.visuals.core.BaseControl);
}());

shmi.pkg("visuals.controls");
/**
 * DialogBox control to display content in an overlay window.
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the DialogBox
 * @param config - configuration of the DialogBox
 */
shmi.visuals.controls.DialogBox = function(element, config) {
    this.element = element;

    /* control configuration */
    this.config = config || {};

    /* parse option attributes from html element */
    this.parseAttributes();
    /* set default options */
    shmi.def(this.config, 'class-name', 'dialog-box');
    shmi.def(this.config, 'template', 'default/dialog-box');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'class-name', 'dialog-box');
    shmi.def(this.config, 'initial-state', 'hidden');
    shmi.def(this.config, 'top-level', false);
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'tab-limit', true);
    shmi.def(this.config, 'close-icon-src', "pics/system/controls/dialog-box/close.svg");
    shmi.def(this.config, 'show-close-button', false);

    /* internal variables */
    this.hidden = true;
    this.active = false;
    this.controls = [];
    this.childrenParsed = false;
    this.bgDiv = null;
    this.contentElement = null;
    this.originalParent = null;
    this.tokens = [];

    this.element.style.visibility = "hidden";

    /* control startup */
    this.startup();
};

shmi.visuals.controls.DialogBox.prototype = {
    uiType: "dialog-box",
    isContainer: true,
    getClassName: function() {
        return "DialogBox";
    },
    events: ["open", "close"],
    /**
     * Initializes the DialogBox
     *
     * @override BaseControl.init
     */
    onTemplate: function(response, failed, onDone) {
        var self = this;
        if (!failed) {
            self.element.innerHTML = response;
            self.contentElement = shmi.getUiElement("dialog-box-content", self.element);
            if (self.contentElement && self.config["content-template"]) {
                var templateUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                    self.config["content-template"] :
                    shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");
                shmi.loadResource(templateUrl, function(contentResponse, contentFailed) {
                    if (!contentFailed) {
                        self.contentElement.innerHTML = contentResponse;
                    }
                    onDone();
                });
            } else {
                onDone();
            }
        } else {
            onDone();
        }
    },
    onRegister: function(onDone) {
        var self = this;

        if (self.contentElement) {
            self.parseChildren(self.contentElement, onDone);
        } else {
            onDone();
        }
    },
    onInit: function() {
        var self = this,
            inl = shmi.requires("visuals.tools.iterate.iterateNodeList"),
            closeButtons = [];

        if (self.contentElement) {
            closeButtons = shmi.getUiElements("iq-button", self.element);
            inl(closeButtons, function(el) {
                var but = null;
                if (el.parentNode && !shmi.testParentChild(self.contentElement, el)) {
                    but = shmi.createControl("iq-button", el, {
                        "show-icon": true,
                        "show-text": false,
                        "icon-src": self.config["close-icon-src"] ?? "pics/system/controls/dialog-box/close.svg",
                        "inline-svg-icon": true,
                        "label": null
                    }, "DIV", "from");
                    but.onClick = function() {
                        self.hide();
                    };
                }
            });
        }

        this.titleElement = shmi.getUiElement('dialog-box-title', this.element);
        if (!this.titleElement) {
            shmi.log('[DialogBox] no dialog-box-title element provided', 1);
        }
        this.frameElement = shmi.getUiElement('dialog-box-frame', this.element);
        if (!this.frameElement) {
            shmi.log('[DialogBox] no dialog-box-frame element provided', 3);
            return;
        }

        /* all required elements found */
        this.hidden = this.config['initial-state'] !== 'visible';
        if (this.hidden) {
            shmi.addClass(this.element, 'hidden');
        }
        this.element.style.visibility = "";

        this.containerElement = shmi.getUiElement('dialog-content', this.element);
        if (!this.containerElement) {
            shmi.log('[DialogBox] no dialog-content element provided', 3);
            return;
        }

        this.element.setAttribute('tabindex', '-1');

        if (this.titleElement) {
            if (this.config.title) {
                this.titleElement.textContent = shmi.localize(this.config.title);
            }
        }

        // If the 'show-close-button' config is set, either show or hide the
        // close button, depending on the configuration. If it's unset, leave
        // the css class alone.
        if (this.config['show-close-button'] === true) {
            this.element.classList.add('show-close-button');
        } else if (this.config['show-close-button'] === false) {
            this.element.classList.remove('show-close-button');
        }
    },
    /**
     * Displays the DialogBox
     *
     */
    show: function() {
        var self = this;
        if (!self.active) {
            shmi.log("[DialogBox] cannot show dialog-box without enabling it first", 3);
            return;
        }
        if (!self.hidden) {
            return;
        }
        self.hidden = false;

        if (self.config['top-level'] === true) {
            self.originalParent = self.element.parentNode;
            self.element = document.body.appendChild(self.element);
        }

        if (self.config['cover-background']) {
            if (self.bgDiv) {
                shmi.removeClass(self.bgDiv, 'hidden');
            } else {
                self.bgDiv = document.createElement('div');
                shmi.addClass(self.bgDiv, 'dialog-box-overlay');
                self.bgDiv.setAttribute('tabindex', '-1');
                document.body.appendChild(self.bgDiv);
                shmi.removeClass(self.bgDiv, 'hidden');
            }
        }

        shmi.removeClass(self.element, 'hidden');

        if (self.config['tab-limit']) {
            // Enable element tabulator
            var tabulator = shmi.requires("visuals.tools.tabulator");
            self.tabLimit = tabulator.setTabParent(self.element);
        }

        self.enableControls();
        self.fire('open', {});
    },
    /**
     * Removes the DialogBox from DOM
     *
     */
    hide: function() {
        var self = this;
        if (this.hidden) {
            return;
        }
        this.hidden = true;
        if (!this.config['leave-controls-enabled']) {
            this.disableControls();
        }
        if (self.config['top-level'] === true) {
            self.element = self.originalParent.appendChild(self.element);
        }
        shmi.addClass(this.element, 'hidden');
        if (self.tabLimit) {
            self.tabLimit.unsetTabParent();
            self.tabLimit = null;
        }
        if (this.config['cover-background']) {
            if (this.bgDiv) {
                shmi.addClass(this.bgDiv, 'hidden');
            }
        }
        this.fire('close', {});
    },
    /**
     * Enables the DialogBox
     *
     */
    onEnable: function() {
        const self = this;

        self.tokens.push(shmi.listen("enable", function(evt) {
            if (evt.source.getName().indexOf(`${self.getName()}.`) === 0) {
                if (evt.source.element.getAttribute("data-function") === "close") {
                    self.tokens.push(evt.source.listen("click", () => {
                        self.hide();
                    }));
                }
            }
        }, { "source.uiType": "iq-button" }));

        if (!self.hidden) {
            /* set hidden status to prevent show() to return early */
            self.hidden = true;
            self.show();
        }
    },
    /**
     * Disables the DialogBox
     *
     */
    onDisable: function() {
        const self = this;

        self.hide();

        // remove overlay background element if present
        if (this.bgDiv) {
            this.bgDiv.parentNode.removeChild(this.bgDiv);
            this.bgDiv = null;
        }

        self.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.tokens = [];
    },
    /**
     * Enables child controls of the DialogBox
     *
     */
    enableControls: function() {
        for (var i = 0; i < this.controls.length; i++) {
            try {
                this.controls[i].enable();
            } catch (exc) {
                console.error("[DialogBox]", "error enabling control:", this.controls[i], this.getName(), "Exception:", exc);
            }
        }
    },
    /**
     * Disables child controls of the DialogBox
     *
     */
    disableControls: function() {
        for (var i = 0; i < this.controls.length; i++) {
            try {
                this.controls[i].disable();
            } catch (exc) {
                console.error("[DialogBox]", "error disabling control:", this.controls[i], this.getName(), "Exception:", exc);
            }
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            classLabel = "[" + self.getClassName() + "] ",
            initToken = null;

        if (!self.initialized) {
            throw new Error(classLabel + "Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        if (!self.contentElement) {
            callback(new Error(classLabel + "Content element missing from template."), controls);
            return;
        }

        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.contentElement, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });

        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive() && !self.hidden) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.DialogBox, shmi.visuals.core.BaseControl);

// version https://88.198.203.47/svn//project/branches/naming_scheme - von Meike am 28.08.2014 (war unver??ndert zu Stand 5_CT2)
// ++ patches, fixes Peer 28.08. ff.

shmi.pkg("visuals.controls");
/**
 * General purpose scroll bar.
 *
 * An instance of this control changes values between min and max and fires the related events.
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {Element} element the base element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.GpScrollBar = function(element, config) {
    // short cuts to static "const" definitions for GPSB
    this.c = shmi.visuals.controls.GpScrollBar.c;
    this.uiElName = shmi.visuals.controls.GpScrollBar.uiElName;
    this.uiCssCl = shmi.visuals.controls.GpScrollBar.uiCssCl;

    this.element = element;

    this.config = config || {};
    /* set default options */
    shmi.def(this.config, "class-name", "gp-scroll-bar");
    shmi.def(this.config, "template", "default/gp-scroll-bar");
    shmi.def(this.config, "name", "Unnamed GpScrollBar");
    shmi.def(this.config, "label", "Label?"); // NYI
    shmi.def(this.config, "max", 100);
    shmi.def(this.config, "min", 0);
    shmi.def(this.config, "precision", 0); // tbd, NYI
    shmi.def(this.config, "step", 0); //, NYI
    shmi.def(this.config, "vertical", true); // currently is true impl. only !!
    shmi.def(this.config, "buttons", true);
    shmi.def(this.config, "inverse", false);

    this.parseAttributes();

    //this.initialized = false;
    this.isMinimized = false; // true means that GBSB is used as indicator only, events are disabled
    this.isEnabled = false;
    this.min = 0;
    this.max = 0;
    this.sliderVal = 0;
    this.sliderWidth = 0;
    this.sliderTrackElem = null;
    this.sliderHandleElem = null;
    this.sliderHandleVisibleElem = null;
    this.sliderHandleMovable = null;
    this.btnUpElem = null;
    this.btnDownElem = null;
    this.sliderHandleMouseListener = null;
    this.sliderHandleTouchListener = null;
    this.sliderTrackMouseListener = null;
    this.sliderTrackTouchListener = null;
    this.btnUpMouseListener = null;
    this.btnUpTouchListener = null;
    this.btnDownMouseListener = null;
    this.btnDownTouchListener = null;
    this.sbEventListeners = {};
    this.enableTO = null;
    this.btnStartAutoRepeatTO = null;
    this.btnProcessAutoRepeatTO = null;
    this.isBtnAutoRepeatProcessing = false;

    // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    this.debug = {};
    this.debug.msgFIFO = [];
    this.testlog_msg("startup");
    // $test !!!!!!!!!!!!!!!

    this.startup();
};

shmi.visuals.controls.GpScrollBar.prototype = {
    /*-- BaseControl method interface --*/
    uiType: "gp-scroll-bar",
    getClassName: function() {
        return "GpScrollBar";
    },
    onInit: function() {
        /* config post processing (boolean and number values) */
        this.config["max"] = shmi.toNumber(this.config["max"]);
        this.config["min"] = shmi.toNumber(this.config["min"]);
        this.config["precision"] = shmi.toNumber(this.config["precision"]);
        this.config["step"] = shmi.toNumber(this.config["step"]);
        this.config["vertical"] = shmi.toBoolean(this.config["vertical"]);
        this.config["buttons"] = shmi.toBoolean(this.config["buttons"]);
        this.config["inverse"] = shmi.toBoolean(this.config["inverse"]);

        /* init all members, get references to the necessary elements */
        this.min = this.config["min"];
        this.max = this.config["max"];
        this.sliderVal = this.min; // 1st valid init
        this.sliderWidth = (this.max - this.min) / 10; // 1st valid init
        this.sliderTrackElem = shmi.getUiElement(this.uiElName.sliderTrack, this.element);
        if (!this.sliderTrackElem) {
            shmi.log("[GpScrollBar] no " + this.uiElName.sliderTrack + " element provided (required)", 3);
            return;
        }
        this.sliderHandleElem = shmi.getUiElement(this.uiElName.sliderHandle, this.element);
        if (!this.sliderHandleElem) {
            shmi.log("[GpScrollBar] no " + this.uiElName.sliderHandle + " provided (required)", 3);
            return;
        }
        this.sliderHandleMovable = new shmi.visuals.gfx.Movable(this.sliderHandleElem);
        shmi.addClass(this.sliderHandleElem, this.uiCssCl.sliderAni);
        this.sliderHandleVisibleElem = shmi.getUiElement(this.uiElName.sliderHandleVisible, this.element);
        if (!this.sliderHandleVisibleElem) {
            shmi.log("[GpScrollBar] no " + this.uiElName.sliderHandleVisibleElem + " provided (required)", 3);
            return;
        }

        if (this.config["buttons"]) {
            shmi.addClass(this.element, this.uiCssCl.withBtns);
            this.btnUpElem = shmi.getUiElement(this.uiElName.btnUp, this.element);
            if (!this.btnUpElem) {
                shmi.log("[GpScrollBar] no " + this.uiElName.btnUp + " element provided (required)", 3);
                return;
            }
            this.btnDownElem = shmi.getUiElement(this.uiElName.btnDown, this.element);
            if (!this.btnDownElem) {
                shmi.log("[GpScrollBar] no " + this.uiElName.btnDown + " element provided (required)", 3);
                return;
            }
        } else {
            shmi.addClass(this.element, this.uiCssCl.withoutBtns);
            var btnContElem = shmi.getUiElement(this.uiElName.btnPanel, this.element);
            if (btnContElem) {
                btnContElem.parentNode.removeChild(btnContElem);
            }
        }

        /* define and route the event handlers, filter events */
        var sliderHandleEvents = {};
        sliderHandleEvents.onDrag = function(dx, dy, event) {
            event.preventDefault();
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderHandleEvents.onDrag(..) fired - dx:" + dx + " dy:" + dy);
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            this.updateVal(this.sliderHandleMovable.ty + dy);
            this.updateSlider();
            this.fireValChanged(this.sliderVal, false);
        }.bind(this);
        sliderHandleEvents.onPress = function() {
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderHandleEvents.onPress() fired");
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            shmi.removeClass(this.sliderHandleElem, this.uiCssCl.sliderAni);
            //shmi.addClass(this.sliderHandleElem, this.uiCssCl.btnSel);
            shmi.addClass(this.sliderHandleVisibleElem, this.uiCssCl.btnSel);
        }.bind(this);
        sliderHandleEvents.onRelease = function() {
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderHandleEvents.onRelease() fired");
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            shmi.addClass(this.sliderHandleElem, this.uiCssCl.sliderAni);
            //shmi.removeClass(this.sliderHandleElem, this.uiCssCl.btnSel);
            shmi.removeClass(this.sliderHandleVisibleElem, this.uiCssCl.btnSel);
        }.bind(this);
        this.sliderHandleMouseListener = new shmi.visuals.io.MouseListener(this.sliderHandleElem, sliderHandleEvents);
        this.sliderHandleTouchListener = new shmi.visuals.io.TouchListener(this.sliderHandleElem, sliderHandleEvents);
        var sliderTrackEvents = {};
        sliderTrackEvents.onPress = function(x, y, event) {
            shmi.addClass(this.sliderTrackElem, this.uiCssCl.trackSel);
        }.bind(this);
        sliderTrackEvents.onRelease = function() {
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderTrackEvents.onRelease() fired");
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            shmi.removeClass(this.sliderTrackElem, this.uiCssCl.trackSel);
        }.bind(this);
        sliderTrackEvents.onClick = function(x, y, event) {
            var pos = shmi.getAbsPosition(this.sliderTrackElem);
            var deltaY = y - pos.y;
            this.updateVal(deltaY);
            this.updateSlider();
            this.fireValChanged(this.sliderVal, true);
        }.bind(this);
        this.sliderTrackMouseListener = new shmi.visuals.io.MouseListener(this.sliderTrackElem, sliderTrackEvents);
        this.sliderTrackTouchListener = new shmi.visuals.io.TouchListener(this.sliderTrackElem, sliderTrackEvents);
        if (this.btnUpElem) {
            var btnUpEvents = {};
            btnUpEvents.onPress = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnUpEvents.onPress() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                shmi.addClass(this.btnUpElem, this.uiCssCl.btnSel);
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.btnStartAutoRepeatTO = setTimeout(function() {
                    this.startBtnAutoRepeat("up");
                }.bind(this), this.c.START_BTN_AUTO_REPEAT_TO);
                this.fireBtnAction("up");
            }.bind(this);
            btnUpEvents.onRelease = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnUpEvents.onRelease() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.stopBtnAutoRepeat();
                shmi.removeClass(this.btnUpElem, this.uiCssCl.btnSel);
            }.bind(this);
            this.btnUpMouseListener = new shmi.visuals.io.MouseListener(this.btnUpElem, btnUpEvents);
            this.btnUpTouchListener = new shmi.visuals.io.TouchListener(this.btnUpElem, btnUpEvents);
            var btnDownEvents = {};
            btnDownEvents.onPress = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnDownEvents.onPress() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                shmi.addClass(this.btnDownElem, this.uiCssCl.btnSel);
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.btnStartAutoRepeatTO = setTimeout(function() {
                    this.startBtnAutoRepeat("down");
                }.bind(this), this.c.START_BTN_AUTO_REPEAT_TO);
                this.fireBtnAction("down");
            }.bind(this);
            btnDownEvents.onRelease = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnDownEvents.onRelease() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.stopBtnAutoRepeat();
                shmi.removeClass(this.btnDownElem, this.uiCssCl.btnSel);
            }.bind(this);
            this.btnDownMouseListener = new shmi.visuals.io.MouseListener(this.btnDownElem, btnDownEvents);
            this.btnDownTouchListener = new shmi.visuals.io.TouchListener(this.btnDownElem, btnDownEvents);
        }
        this.resizeListener = function() {
            this.processEvtOnResize();
        }.bind(this);

        shmi.log("[GpScrollBar] initialized", 1);
    },
    onEnable: function() {
        // test !!!!!!!!!!!!!!!
        this.testlog_msg("onEnable");
        // test !!!!!!!!!!!!!!!
        window.addEventListener("resize", this.resizeListener, true);
        this.updateSlider();
        this.isEnabled = true;
        this.updateEvtEnable();
        shmi.log("[GpScrollBar] enabled", 1);
    },
    onDisable: function() {
        // test !!!!!!!!!!!!!!!
        this.testlog_msg("onEnable");
        // test !!!!!!!!!!!!!!!
        window.removeEventListener("resize", this.resizeListener, true);
        this.isEnabled = false;
        this.updateEvtEnable();
        shmi.log("[GpScrollBar] disabled", 1);
    },
    onDelete: function() {},

    /*-- additional method interface --*/

    /*
     * $todo: noch JSDoc method descriptions add
     *
     * val, width are numbers in the range of [min .. max]
     */
    setSliderVal: function(val) {
        this.sliderVal = val;
        this.updateSlider();
        shmi.log("[GpScrollBar] setSliderVal called, val: " + val, 1);
    },
    setSliderWidth: function(width) {
        this.sliderWidth = width;
        this.updateSlider();
        shmi.log("[GpScrollBar] setSliderWidth called, width: " + width, 1);
    },
    setSliderProps: function(val, width) {
        this.sliderVal = val;
        this.sliderWidth = width;
        this.updateSlider();
        shmi.log("[GpScrollBar] setSliderProps called, val: " + val + " - width: " + width, 1);
    },
    getSliderVal: function() {
        shmi.log("[GpScrollBar] getSliderVal called, return: " + this.sliderVal, 1);
        return this.sliderVal;
    },
    getSliderWidth: function() {
        shmi.log("[GpScrollBar] getSliderVal called, return: " + this.sliderWidth, 1);
        return this.sliderWidth;
    },
    setMin: function(min) {
        this.min = min;
        this.updateSlider();
        shmi.log("[GpScrollBar] setMin called, min: " + min, 1);
    },
    setMax: function(max) {
        this.max = max;
        this.updateSlider();
        shmi.log("[GpScrollBar] setMax called, min: " + max, 1);
    },
    setMinMax: function(min, max) {
        this.min = min;
        this.max = max;
        this.updateSlider();
        shmi.log("[GpScrollBar] setMinMax called, min: " + min + " - max: " + max, 1);
    },
    getMin: function() {
        shmi.log("[GpScrollBar] getMin called, return: " + this.min, 1);
        return this.min;
    },
    getMax: function() {
        shmi.log("[GpScrollBar] getMax called, return: " + this.max, 1);
        return this.max;
    },
    setAllProps: function(val, width, min, max) {
        this.sliderVal = val;
        this.sliderWidth = width;
        this.min = min;
        this.max = max;
        this.updateSlider();
        shmi.log("[GpScrollBar] setAllProps called, , val: " + val + " - width: " + width + " - min: " + min + " - max: " + max, 1);
    },
    setMinimized: function(min) {
        this.isMinimized = min;
        this.updateEvtEnable();
        if (min) {
            shmi.removeClass(this.element, this.uiCssCl.normal);
            shmi.addClass(this.element, this.uiCssCl.minimized);
        } else {
            shmi.removeClass(this.element, this.uiCssCl.minimized);
            shmi.addClass(this.element, this.uiCssCl.normal);
        }
    },
    getMinimized: function() {
        return this.isMinimized;
    },

    /*-- event listener control interface --*/

    /*  $todo: noch JSDoc method descriptions add
     *
     *  note: listenerObj must implement the event handler callbacks
     *   .gpsbOnValChange(val) (val in range [min..max])
     *   .gpsbOnBtnAction(btn)  (btn in range ["up", "down"])
     */
    addEventListener: function(listenerObj) {
        var id = Date.now();
        while (this.sbEventListeners[id] !== undefined) {
            id++;
        }
        this.sbEventListeners[id] = {};
        this.sbEventListeners[id].listener = listenerObj;
        shmi.log("[GpScrollBar] addEventListener called, listenerObj: " + listenerObj + " - id:" + id, 1);
        return id;
    },
    removeEventListener: function(id) {
        if (this.sbEventListeners[id] !== undefined) {
            delete this.sbEventListeners[id];
        } else {
            shmi.log("[GpScrollBar] removeEventListener - id " + id + " does not exist", 2);
        }
        shmi.log("[GpScrollBar] removeEventListener called, id: " + id, 1);
    },

    /*-- private helper methods - usage within GPSB only !! --*/

    /**
     * @private
     */
    startBtnAutoRepeat: function(btn) {
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //console.log(">>> startBtnAutoRepeat() - btn: " + btn);
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        this.isBtnAutoRepeatProcessing = true;
        if (this.btnProcessAutoRepeatTO) {
            clearTimeout(this.btnProcessAutoRepeatTO);
        }
        this.btnProcessAutoRepeatTO = setTimeout(function() {
            this.processBtnAutoRepeat(btn);
        }.bind(this), this.c.PROCESS_BTN_AUTO_REPEAT_TO);
    },
    processBtnAutoRepeat: function(btn) {
        if (this.isBtnAutoRepeatProcessing) {
            // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log(">>> processBtnAutoRepeat() - btn: " + btn);
            // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            this.fireBtnAction(btn);
            this.btnProcessAutoRepeatTO = setTimeout(function() {
                this.processBtnAutoRepeat(btn);
            }.bind(this), this.c.PROCESS_BTN_AUTO_REPEAT_TO);
        }
    },
    /**
     * @private
     */
    stopBtnAutoRepeat: function() {
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //console.log("<<< stopBtnAutoRepeat()");
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        this.isBtnAutoRepeatProcessing = false;
        if (this.btnProcessAutoRepeatTO) {
            clearTimeout(this.btnProcessAutoRepeatTO);
        }
    },
    /**
     * @private
     */
    processEvtOnResize: function() {
        this.updateSlider();
        //console.log("GpScrollBar processEvtOnResize");
    },
    /**
     * @private
     */
    updateSlider: function() {
        if (this.initialized) {
            var slHeight = (this.sliderWidth * this.sliderTrackElem.offsetHeight) / (this.max - this.min);
            if (this.isMinimized) {
                if (slHeight < this.c.MIN_SLIDER_SIZE_PX_MINIMIZED) {
                    slHeight = this.c.MIN_SLIDER_SIZE_PX_MINIMIZED;
                }
            } else if (slHeight < this.c.MIN_SLIDER_SIZE_PX) {
                slHeight = this.c.MIN_SLIDER_SIZE_PX;
            }
            var slAvailTrackHeight = this.sliderTrackElem.offsetHeight - slHeight,
                slTop = ((this.max - this.sliderVal) * slAvailTrackHeight) / (this.max - this.min);

            // $todo: vor Einstellung der neuen Slider-TopPos/-Height -> range it ??
            this.sliderHandleElem.style.height = slHeight + "px";
            if (!this.config["inverse"]) {
                slTop = slAvailTrackHeight - slTop;
            }
            this.sliderHandleMovable.translate(0, slTop - this.sliderHandleMovable.ty);
        }
    },
    /**
     * @private
     */
    updateVal: function(newSlTop) {
        if (this.initialized) {
            var newVal = this.max - (newSlTop * (this.max - this.min)) / (this.sliderTrackElem.offsetHeight - this.sliderHandleElem.offsetHeight);
            if (!this.config["inverse"]) {
                newVal = this.max - newVal;
            }
            if (newVal < this.min) {
                this.sliderVal = this.min;
            } else if (newVal > this.max) {
                this.sliderVal = this.max;
            } else {
                this.sliderVal = newVal;
            }
        }
    },
    /**
     * @private
     */
    updateEvtEnable: function() {
        var enabled = (this.isEnabled && !this.isMinimized);
        if (this.initialized) {
            // test !!!!!!!!!!!!!!!
            this.testlog_msg("init ok - en: " + this.isEnabled + " - min: " + this.isMinimized);
            // test !!!!!!!!!!!!!!!
            if (enabled) {
                this.sliderHandleMouseListener.enable();
                this.sliderHandleTouchListener.enable();
                this.sliderTrackMouseListener.enable();
                this.sliderTrackTouchListener.enable();
                if (this.btnUpElem) {
                    this.btnUpMouseListener.enable();
                    this.btnUpTouchListener.enable();
                    this.btnDownMouseListener.enable();
                    this.btnDownTouchListener.enable();
                }
            } else {
                this.sliderHandleMouseListener.disable();
                this.sliderHandleTouchListener.disable();
                this.sliderTrackMouseListener.disable();
                this.sliderTrackTouchListener.disable();
                if (this.btnUpElem) {
                    this.btnUpMouseListener.disable();
                    this.btnUpTouchListener.disable();
                    this.btnDownMouseListener.disable();
                    this.btnDownTouchListener.disable();
                }
            }
        } else {
            // test !!!!!!!!!!!!!!!
            this.testlog_msg("init ! ok - repeat");
            // test !!!!!!!!!!!!!!!
            if (this.enableTO) {
                clearTimeout(this.enableTO);
            }
            this.enableTO = setTimeout(function() {
                this.updateEvtEnable();
            }.bind(this), this.c.ENABLE_TO);
        }
    },
    /**
     * @private
     */
    fireValChanged: function(val, fromTrack) {
        const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

        iterObj(this.sbEventListeners, (listener) => {
            listener.listener.gpsbOnValChange(val, fromTrack);
        });
    },
    /**
     * @private
     */
    fireBtnAction: function(btn) {
        const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

        iterObj(this.sbEventListeners, (listener) => {
            listener.listener.gpsbOnBtnAction(btn);
        });
    },

    // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    /**
     * @private
     */
    testlog_msg: function(msg) {
        var msgDisplay = document.getElementById("debugDisplEvents");
        if (msgDisplay) {
            var displInfo = [];
            msgDisplay.innerHTML = "";
            displInfo.push("last debug messages:");
            displInfo.push("---");
            console.log(msg);
            this.debug.msgFIFO.push(msg);
            if (this.debug.msgFIFO.length > 10) {
                this.debug.msgFIFO.shift();
            }

            for (var i = this.debug.msgFIFO.length - 1; i >= 0; i--) {
                displInfo.push(this.debug.msgFIFO[i]);
            }
            displInfo.push("--------------------------------");
            for (i = 0; i < displInfo.length; i++) {
                msgDisplay.innerHTML += displInfo[i] + "<br>";
            }
        }
    }
};

shmi.extend(shmi.visuals.controls.GpScrollBar, shmi.visuals.core.BaseControl);
shmi.registerControlType("gp-scroll-bar", shmi.visuals.controls.GpScrollBar, true);

// $todo Meike begin - ++++++++++++++++
// $todo -> hier ggfs. element names und class names anpassen fuer Vereinheitlichung/ueberarbeitung CSS/Templates der Controls
shmi.visuals.controls.GpScrollBar.uiElName = {};
(function() {
    var __gpsbsc = shmi.visuals.controls.GpScrollBar.uiElName;
    /* The following ui element names are used in CT2's JS code. Change it here if necessary.
     All other ui element names may be changed in the CT2 template(s).
     */
    __gpsbsc.sliderTrack = "gpsb-slider-track";
    __gpsbsc.sliderHandle = "gpsb-slider-handle"; // this is the slider touch area
    __gpsbsc.sliderHandleVisible = "gpsb-slider-handle-visible"; // this is the visible slider button
    __gpsbsc.btnPanel = "gpsb-btn-panel";
    __gpsbsc.btnUp = "gpsb-btn-up";
    __gpsbsc.btnDown = "gpsb-btn-down";

    shmi.visuals.controls.GpScrollBar.uiCssCl = {};
    __gpsbsc = shmi.visuals.controls.GpScrollBar.uiCssCl;
    /* The following ui element class names are used in CT2's JS code. Change it here if necessary.
     All other ui element class names be changed in the CT2 template(s).
     */
    // dynamically switched styles
    __gpsbsc.withBtns = "gpsb-with-btns";
    __gpsbsc.withoutBtns = "gpsb-without-btns";
    __gpsbsc.btnSel = "gpsb-btn-selected";
    __gpsbsc.trackSel = "gpsb-track-selected";
    __gpsbsc.minimized = "gpsb-minimized";
    __gpsbsc.normal = "gpsb-normal";
    // transitions/animations
    __gpsbsc.sliderAni = "gpsb-slider-ani";
    // $todo Meike end - ++++++++++++++++

    shmi.visuals.controls.GpScrollBar.c = {};
    __gpsbsc = shmi.visuals.controls.GpScrollBar.c;
    __gpsbsc.MIN_SLIDER_SIZE_PX = 60;
    __gpsbsc.MIN_SLIDER_SIZE_PX_MINIMIZED = 5;
    __gpsbsc.ENABLE_TO = 500;
    __gpsbsc.START_BTN_AUTO_REPEAT_TO = 1000;
    __gpsbsc.PROCESS_BTN_AUTO_REPEAT_TO = 100;
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new control Group
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control group
 * @param config - configuration of the control group
 */
shmi.visuals.controls.Group = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'group');
    shmi.def(this.config, 'template', null);
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'replacers', {});

    this.initialized = false;
    this.controls = [];

    this.startup();
};

shmi.visuals.controls.Group.prototype = {
    uiType: "group",
    isContainer: true,
    getClassName: function() {
        return "Group";
    },
    /**
     * parses child controls
     */
    onRegister: function(onDone) {
        var self = this;
        self.parseChildren(self.element, onDone);
    },
    /**
     * initializes control
     */
    onInit: function() {},
    /**
     * Enables the Group
     *
     */
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }
        shmi.log("[Group] enabled", 1);
    },
    /**
     * Disables the Group
     *
     */
    onDisable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].disable();
        }
        shmi.log("[Group] disabled", 1);
    },
    /**
     * Locks the Group
     *
     */
    onLock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].lock();
        }
        shmi.addClass(this.element, 'locked');
        shmi.log("[Group] locked", 1);
    },
    /**
     * Unlocks the Group
     *
     */
    onUnlock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].unlock();
        }

        shmi.removeClass(this.element, 'locked');
        shmi.log("[Group] locked", 1);
    },
    /**
     * Sets value to the Group
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        shmi.log("[Group] value set: " + value, 0);
    }
};

shmi.extend(shmi.visuals.controls.Group, shmi.visuals.core.BaseControl);

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "html",
 *     "name": null,
 *     "template": "default/html"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "Html", // control name in camel-case
        uiType = "html", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "html",
        "name": null,
        "template": "default/html",
        "use-shadow-dom": false,
        "html": ""
    };

    // declare private functions - START
    /**
     * Parses an html string and creates a fragment which can be attached to
     * another node.
     *
     * @param {string} html HTML string to parse.
     * @returns {DocumentFragment}
     */
    function documentFragmentFromHTML(html) {
        const templateNode = document.createElement("template");

        templateNode.innerHTML = html;

        return templateNode.content.cloneNode(true);
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            html: null
        },
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    htmlContentNode = shmi.getUiElement("html", self.element);

                if (!htmlContentNode) {
                    console.log('Error: "html" element is missing in template!');
                    return;
                }

                // Get the node to which to append the widgets contents.
                const rootNode = (() => {
                    if (self.config["use-shadow-dom"]) {
                        return htmlContentNode.attachShadow({ mode: "open" });
                    }

                    return htmlContentNode;
                })();

                // Store a reference to the root node to preserve backward
                // compatibility with code that does something it's not
                // supposed to (reading "private" widget state).
                self.vars.html = rootNode;

                // Parse the content and attach it to the root node.
                rootNode.appendChild(documentFragmentFromHTML(self.config.html));
            },

            /**
             * Returns the root of the HTML content.
             *
             * @returns {Node & InnerHTML & ParentNode}
             */
            getRootNode: function() {
                return this.vars.html;
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iframe",
 *     "name": null,
 *     "template": "default/iframe"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "Iframe", // control name in camel-case
        uiType = "iframe", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "iframe",
        "name": null,
        "template": "default/iframe ",
        "src": "about:blank",
        "disable-scrollbar": false
    };

    // declare private functions - START

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            iframe: null
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;
                self.vars.iframe = self.element.querySelector("iframe");
                if (self.vars.iframe) {
                    self.vars.iframe.onerror = function(e) {
                        shmi.notify("Error loading IFrame: " + e.toString());
                    };
                    self.vars.iframe.setAttribute("src", self.config.src);
                    if (self.config["disable-scrollbar"]) {
                        self.vars.iframe.setAttribute("scrolling", "no");
                        self.vars.iframe.style.overflow = 'hidden';
                    }
                }
            },
            /* called when control is enabled */
            onEnable: function() {

            },
            /* called when control is disabled */
            onDisable: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                if (self.vars.iframe) {
                    self.vars.iframe.style.pointerEvents = "none";
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                if (self.vars.iframe) {
                    self.vars.iframe.style.pointerEvents = "";
                }
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Control IQ Alarm-Info
 *
 * Configuration options (default):
 *
 * {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-alarm-info",
        "noAlarm": null,
        "action": null,
        "enableCycle": true,
        "cycleInterval": 1000,
        "showAlarm": true,
        "showWarn": true,
        "showInfo": true,
        "groupFilter": null,
        "icon-info": "pics/system/controls/iq-alarm-info/icon-info.svg",
        "icon-warning": "pics/system/controls/iq-alarm-info/icon-warning.svg",
        "icon-alarm": "pics/system/controls/iq-alarm-info/icon-alarm.svg"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    const className = "IQAlarmInfo", //control name in camel-case
        uiType = "iq-alarm-info", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-alarm-info",
        "noAlarm": null,
        "action": null,
        "enableCycle": true,
        "cycleInterval": 1000,
        "showAlarm": true,
        "showWarn": true,
        "showInfo": true,
        "groupFilter": null,
        "icon-info": "pics/system/controls/iq-alarm-info/icon-info.svg",
        "icon-warning": "pics/system/controls/iq-alarm-info/icon-warning.svg",
        "icon-alarm": "pics/system/controls/iq-alarm-info/icon-alarm.svg",
        "icon-idle": null
    };

    //setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        log = logger.log;

    // Constants and private fields
    const ALARM_STATE_SELECTORS = ["severity-info", "severity-warning", "severity-alarm"];

    //declare private functions - START

    /**
     * parseAlarmGroupRange - parse range of alarm groups
     *
     * @param {string} rangeText input text (e.g. `3-7`)
     * @returns {number[2]|null} range start & end tuple or `null` if none could be parsed
     */
    function parseAlarmGroupRange(rangeText) {
        const tuple = rangeText.split("-");

        if (tuple.length === 2) {
            const start = parseInt(tuple[0]),
                end = parseInt(tuple[1]);

            if (!(isNaN(start) || isNaN(end)) && start >= 0 && end >= start) {
                return [start, end];
            }
        }

        return null;
    }

    /**
     * parseAlarmGroupFilter - parse group ID filter from config string
     *
     * @param {string} groupFilter comma separated group ID filter
     * @returns {array} array of group IDs and ranges
     */
    function parseAlarmGroupFilter(groupFilter) {
        if (typeof groupFilter !== "string") {
            return null;
        }

        let entries = groupFilter.split(",");
        entries = entries.map((entry) => {
            if (entry.includes("-")) {
                return parseAlarmGroupRange(entry);
            }

            const groupId = parseInt(entry);
            if (!isNaN(groupId) && groupId >= 0) {
                return groupId;
            }

            return null;
        });
        entries = entries.filter((entry) => entry !== null);
        const rangeEntries = [];
        entries.forEach((entry) => {
            if (Array.isArray(entry)) {
                let idx = entry[0];
                while (idx <= entry[1]) {
                    if (!(rangeEntries.includes(idx) && entries.includes(idx))) {
                        rangeEntries.push(idx);
                    }
                    idx += 1;
                }
            }
        });
        entries.push(...rangeEntries);

        return entries.length ? entries : null;
    }

    /**
     * testAlarmState - test & update state of displayed alarm
     *
     * @param {object} self instance reference
     */
    function testAlarmState(self) {
        const alarmArray = Object.values(self.imports.am.alarms).map((value) => value.properties);
        if (Array.isArray(alarmArray)) {
            if (alarmArray.length < 1) {
                clearAlarmMsg(self);
            } else {
                clearInt(self);
                if (!self.config.enableCycle) {
                    // standard mode: lookup for last Alarm of highest class
                    self.vars.activeAlarmList = getLastHighest(self, alarmArray);
                } else {
                    // cycleMode: cycle display of all active alarms
                    self.vars.activeAlarmList = getEnabledAlarms(self, alarmArray);
                    self.vars.alarmCount = self.vars.activeAlarmList.length;
                    if (self.vars.alarmCount > 1) {
                        self.vars.interval = setInterval(displayAlarms.bind(null, self), self.config.cycleInterval);
                    }
                }
                displayAlarms(self);
            }
        }
    }

    // helper: displays alarms in activeAlarmList
    function displayAlarms(self) {
        if (self.vars.activeAlarmList.length === 0) {
            clearAlarmMsg(self);
        }
        if (self.vars.currentAlarm >= self.vars.activeAlarmList.length) {
            self.vars.currentAlarm = 0;
        }
        const alarm = self.vars.activeAlarmList[self.vars.currentAlarm++];
        let msg = null,
            details = "";
        if (alarm) { // evaluate context items etc.
            msg = shmi.evalString(shmi.localize(`\${alarm_title_${alarm.index}}`), alarm);
            details = shmi.evalString(shmi.localize(`\${alarm_msg_${alarm.index}}`), alarm);
        } else {
            msg = shmi.localize(self.config.noAlarm);
        }
        setAlarmStyle(alarm ? alarm.severity : -1, self);
        setTextContent(self, "alarmMessage", msg);
        setTextContent(self, "alarmDetails", details);
        setTextContent(self, "alarmCount", self.vars.alarmCount);
    }

    // helper: get alarm object of the last Alarm and highest class (=severity)
    function getLastHighest(self, alarmList) {
        let lastAlarm = null;
        const enabledAlarms = getEnabledAlarms(self, alarmList);
        self.vars.alarmCount = enabledAlarms.length;
        enabledAlarms.forEach(function(alarmInfo) {
            // Find latest alarm with the highest occurring severity
            if (lastAlarm === null) {
                lastAlarm = alarmInfo;
            } else if (alarmInfo.severity >= lastAlarm.severity && alarmInfo.timestamp_in > lastAlarm.timestamp_in) {
                lastAlarm = alarmInfo;
            }
        });

        return lastAlarm === null ? [] : [lastAlarm];
    }

    // helper: select alarms to be displayed dependig on the config.notShowWarn and config.notShowInfo parameters
    function getEnabledAlarms(self, alarmList) {
        const enabledAlarms = alarmList.filter(function(alarmInfo) {
            if (Array.isArray(self.vars.groupFilter)) {
                if (!self.vars.groupFilter.includes(alarmInfo.group)) {
                    return false;
                }
            }

            //filter inactive alarms
            if (!alarmInfo.active && (!alarmInfo.acknowledgeable || alarmInfo.acknowledged)) {
                return false;
            }

            // Filter alarms based on control configuration
            return (alarmInfo.severity === 2 && self.config.showAlarm) ||
                (alarmInfo.severity === 1 && self.config.showWarn) ||
                (alarmInfo.severity === 0 && self.config.showInfo);
        });

        return enabledAlarms;
    }

    // helper: clears the alarm message
    function clearAlarmMsg(self) {
        clearInt(self);
        self.vars.currentAlarm = 0;
        setAlarmStyle(-1, self);
        setTextContent(self, "alarmMessage", shmi.localize(self.config.noAlarm));
    }

    // helper: clears the interval
    function clearInt(self) {
        if (self.vars.interval) {
            clearInterval(self.vars.interval);
            self.vars.interval = null;
        }
    }

    /**
     * setAlarmIcon - set active alarm state icon, hide icon if none set
     *
     * @param {object} self control instance
     * @param {string} optionName icon option name
     */
    function setAlarmIcon(self, optionName) {
        const { applyIcon } = shmi.requires("visuals.tools.icons");

        if (self.vars.elements.stateIcon) {
            if (self.config[optionName]) {
                self.vars.elements.stateIcon.style.visibility = "";
                applyIcon(self.vars.elements.stateIcon, self.config[optionName], null, true);
            } else {
                self.vars.elements.stateIcon.style.visibility = "hidden";
                self.vars.elements.stateIcon.style.backgroundImage = "";
            }
        }
    }
    // helper: sets alarm class (state)
    function setAlarmStyle(state, self) {
        const button = self.vars.elements.button;

        switch (state) {
        case 0:
            setAlarmIcon(self, "icon-info");
            break;
        case 1:
            setAlarmIcon(self, "icon-warning");
            break;
        case 2:
            setAlarmIcon(self, "icon-alarm");
            break;
        default:
            setAlarmIcon(self, "icon-idle");
        }
        if (button) {
            shmi.removeClass(button, ALARM_STATE_SELECTORS.join(" "));
            if (ALARM_STATE_SELECTORS[state]) {
                shmi.addClass(button, ALARM_STATE_SELECTORS[state]);
            }
        }
    }

    /**
     * setTextContent - set text content of specified element
     *
     * @param {object} self control instance
     * @param {string} elementName name of element
     * @param {string} text text to set
     */
    function setTextContent(self, elementName, text) {
        if (self.vars.elements[elementName]) {
            self.vars.elements[elementName].textContent = text;
        }
    }
    //declare private functions - END

    //definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            elements: {
                stateIcon: null,
                alarmCount: null,
                alarmMessage: null,
                button: null
            },
            alarmCount: 0,
            listeners: [],
            tokens: [],
            activeAlarmList: [],
            currentAlarm: 0,
            interval: 0,
            alarmSubscriber: null,
            groupFilter: null
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via function call */
            am: "visuals.session.AlarmManager"
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.groupFilter = parseAlarmGroupFilter(self.config.groupFilter);

                self.vars.elements.alarmCount = shmi.getUiElement('alarm-count', self.element);
                if (!self.vars.elements.alarmCount) {
                    log(uiType, "alarm-count not found in template");
                }

                self.vars.elements.stateIcon = shmi.getUiElement('state-icon', self.element);
                if (!self.vars.elements.stateIcon) {
                    log(uiType, "state-icon not found in template");
                }

                self.vars.elements.alarmMessage = shmi.getUiElement('alarm-message', self.element);
                if (!self.vars.elements.alarmMessage) {
                    log(uiType, "alarm-message not found in template");
                } else {
                    setTextContent(self, "alarmMessage", shmi.localize(self.config.noAlarm));
                }

                self.vars.elements.alarmDetails = shmi.getUiElement('alarm-details', self.element);
                if (!self.vars.elements.alarmDetails) {
                    log(uiType, "alarm-details not found in template");
                } else {
                    setTextContent(self, "alarmDetails", "");
                }

                self.vars.elements.button = shmi.getUiElement('button-element', self.element);
                if (self.vars.elements.button && Array.isArray(self.config.action) && self.config.action.length) {
                    const core = shmi.requires("visuals.core"),
                        io = shmi.requires("visuals.io"),
                        action = new core.UiAction(self.config.action),
                        handler = {
                            onClick: () => action.execute()
                        },
                        ml = new io.MouseListener(self.vars.elements.button, handler), //MouseListener
                        tl = new io.TouchListener(self.vars.elements.button, handler); //TouchListener

                    shmi.addClass(self.element, "has-action");
                    self.vars.listeners.push(ml, tl);
                } else if (!self.vars.elements.button) {
                    log(uiType, "click element not found in template");
                }

                setAlarmStyle(-1, self);
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.stateIcons = shmi.getUiElements('state', self.element) || [];
                self.vars.alarmSubscriber = self.imports.am.subscribeAlarms(self, (alarm, isLast) => {
                    if (isLast) {
                        testAlarmState(self);
                    }
                });
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.imports.am.unsubscribeAlarms(self.vars.alarmSubscriber);
                clearInt(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                shmi.addClass(self.element, "locked");
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.removeClass(self.element, "locked");
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-alarm-list
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-alarm-list",
 *     "name": null,
 *     "template": "default/iq-alarm-list"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */

/*eslint-env node */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-alarm-list", // control name in camel-case
        uiType = "iq-alarm-list", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-alarm-list",
        "name": null,
        "template": "default/iq-alarm-list.iq-variant-01",
        "group-filter": null,
        "alarm-image": "pics/system/controls/iq-alarm-list/icon-alarm.svg",
        "warn-image": "pics/system/controls/iq-alarm-list/icon-warning.svg",
        "notify-image": "pics/system/controls/iq-alarm-list/icon-info.svg",
        "dateformat": "${alarmlist_dateformat}",
        "allow-mode-change": false,
        "allow-group-filter-change": false,
        "display-mode": "all",
        "disable-commit": false,
        "detail-template": null,
        "comment-mode": "disabled",
        "columnConfig": {
            "col1": {
                "fields": ["level"],
                "column-width": "60px"
            },
            "col2": {
                "fields": ["id"],
                "column-width": "50px"
            },
            "col3": {
                "fields": ["description"]
            },
            "col4": {
                "fields": ["come"],
                "column-width": "150px"
            },
            "col5": {
                "fields": ["group"],
                "column-width": "150px"
            },
            "col6": {
                "fields": ["isAck"],
                "column-width": "100px"
            },
            "col7": {
                "fields": ["json"],
                "column-width": "90px"
            }
        },
        "inline-svg": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START

    /**
     * parseAlarmGroupRange - parse range of alarm groups
     *
     * @param {string} rangeText input text (e.g. `3-7`)
     * @returns {number[2]|null} range start & end tuple or `null` if none could be parsed
     */
    function parseAlarmGroupRange(rangeText) {
        const tuple = rangeText.split("-");

        if (tuple.length === 2) {
            const start = parseInt(tuple[0]),
                end = parseInt(tuple[1]);

            if (!(isNaN(start) || isNaN(end)) && start >= 0 && end >= start) {
                return [start, end];
            }
        }

        return null;
    }

    /**
     * parseAlarmGroupFilter - parse group ID filter from config string
     *
     * @param {string} groupFilter comma separated group ID filter
     * @returns {array} array of group IDs and ranges
     */
    function parseAlarmGroupFilter(groupFilter) {
        if (typeof groupFilter !== "string") {
            return null;
        }

        const entries = groupFilter.split(",").map((entry) => {
            if (entry.includes("-")) {
                return parseAlarmGroupRange(entry);
            }

            const groupId = parseInt(entry);
            if (!isNaN(groupId) && groupId >= 0) {
                return groupId;
            }

            return null;
        });

        const out = [];
        entries.forEach((entry) => {
            if (Array.isArray(entry)) {
                let idx = entry[0];
                while (idx <= entry[1]) {
                    if (!(out.includes(idx) && entries.includes(idx))) {
                        out.push(idx);
                    }
                    idx += 1;
                }
            } else if (typeof entry === "number") {
                out.push(entry);
            }
        });

        return out.length ? out : null;
    }

    /**
     * Observes startup-sequence of control
     *
     * @param {object} self Control-Obj
     * @returns {bool} Startup succeeded or not
     */
    function controlStartUp(self) {
        let anchorsFound = false,
            controlsCreated = false;
        const io = shmi.requires("visuals.io");

        self.vars.timeFilterLabel = shmi.getUiElement("filter-label", self.element);
        self.vars.activeFilterLabel = shmi.getUiElement("time-frame-label", self.element);
        self.vars.activeFilterFrom = shmi.getUiElement("start-date", self.element);
        self.vars.activeFilterTo = shmi.getUiElement("end-date", self.element);
        self.vars.resetButton = shmi.getUiElement("reset-button", self.element);
        self.vars.detailsRight = shmi.getUiElement("alarmListDetails", self.element);

        if (!self.vars.timeFilterLabel || !self.vars.activeFilterLabel || !self.vars.activeFilterFrom || !self.vars.activeFilterTo || !self.vars.resetButton) {
            return false;
        }

        self.vars.timeFilterLabel.textContent = shmi.localize("${alarmlist_set_timefilter}");

        const resetButtonHandler = {
            onClick: function() {
                clearFilter(self);
            }
        };
        self.vars.listeners.push(
            new io.MouseListener(self.vars.resetButton, resetButtonHandler),
            new io.TouchListener(self.vars.resetButton, resetButtonHandler)
        );

        self.vars.configs.alarmTableConfig["default-field-control-map"]["come"].config["display-format"] = self.config.dateformat;
        initAlarmTableGrid(self);

        anchorsFound = gatherAnchors(self);
        if (anchorsFound) controlsCreated = createChildControls(self);

        if (!self.config['allow-mode-change']) {
            self.vars.anchorEls.modeSelectBox.style.display = "none";
        }

        if (!self.config['allow-group-filter-change'] && self.vars.anchorEls.filterGroupSelectBox) {
            self.vars.anchorEls.filterGroupSelectBox.style.display = "none";
        }

        if (self.config["disable-commit"]) {
            shmi.addClass(self.vars.controls.confirmAllAlarmsButton.element, "hidden locked");
        }

        if (!self.config["comment-mode"] || self.config["comment-mode"] === "disabled") {
            shmi.addClass(self.vars.controls.createThreadButton.element, "hidden locked");
        }

        const groupFilter = parseAlarmGroupFilter(self.config["group-filter"]);
        self.vars.groupFilter = groupFilter;
        if (groupFilter) {
            self.vars.grid.setFilter(7, groupFilter);
        }

        return !(!anchorsFound || !controlsCreated);
    }

    /**
     * generates a random id
     *
     * @returns {string}
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * Manages to set or remove Filter on datagrid
     *
     * @param {object} self Control-Obj
     */
    function updateTableFilter(self) {
        const filterSet = self.vars.filterSet,
            valAr = [];

        if (filterSet.error) valAr.push(2);
        if (filterSet.warn) valAr.push(1);
        if (filterSet.advise) valAr.push(0);

        self.vars.controls.alarmList.resetScroll();
        if (valAr.length > 0) {
            self.vars.grid.setFilter(3, valAr);
        } else {
            self.vars.grid.clearFilter(3);
        }
    }

    /**
     * Checks if needed grid already exists, if not grid will be created
     *
     * @param {object} self Control-Obj
     */
    function initAlarmTableGrid(self) {
        do {
            self.vars.dataGridId = "iqalarms-" + getRandomId();
        } while (self.imports.dgm.getGrid(self.vars.dataGridId) !== null);

        self.imports.dgm.grids[self.vars.dataGridId] = new shmi.visuals.core.DataGridIQAlarms(self.config["display-mode"], self.vars.dataGridId);
        self.vars.grid = self.imports.dgm.getGrid(self.vars.dataGridId);
    }

    /**
     * toggle The filter set between true and false
     *
     * @param {object} self Control-Obj
     * @param {string} type severity - 'error' or 'warn' or 'advise'
     */
    function toggleFilterSet(self, type) {
        const filterSet = self.vars.filterSet;
        filterSet[type] = !filterSet[type];
    }

    /**
     * Sets needed listener for all childcontrols etc.
     *
     * @param {object} self Control-Obj
     * @returns {bool}
     */
    function setListener(self) {
        const { controls, tokens: listeners } = self.vars,
            { showAlarmDetails, openCreateThreadDialogForAlarmInfo } = shmi.requires("visuals.tools.alarms.ls.alarmDetailsIQ");

        if (!controls.confirmAllAlarmsButton || !controls.createThreadButton || !controls.modeSelectBox || !controls.filterCheckBoxError || !controls.filterCheckBoxWarn || !controls.filterCheckBoxAdvise) {
            return false;
        }

        listeners.push(controls.confirmAllAlarmsButton.listen("click", function(event) {
            const filters = self.vars.grid.getFilters();

            if (filters && Array.isArray(filters[7])) {
                self.imports.am.ackAlarmGroups(filters[7]);
            } else {
                self.imports.am.ackAlarm(-1);
            }
        }));

        listeners.push(controls.createThreadButton.listen("click", async function(event) {
            const rowData = self.vars.grid.getRowData(controls.alarmList.selection.selRows[0]);

            if (rowData) {
                const alarmInfo = JSON.parse(rowData[8].value);
                await openCreateThreadDialogForAlarmInfo(self, alarmInfo);

                if (self.vars.detailsRight) {
                    showAlarmDetails(alarmInfo, "inline", self.vars.detailsRight, self);
                }
            }
        }));

        if (controls.filterGroupSelectBox) {
            listeners.push(controls.filterGroupSelectBox.listen("change", function(event) {
                const { groupFilter } = self.vars;

                if (event.detail && event.detail.value !== null) {
                    self.vars.grid.setFilter(7, [event.detail.value]);
                } else if (groupFilter) {
                    self.vars.grid.setFilter(7, groupFilter);
                } else {
                    self.vars.grid.clearFilter(7);
                }
            }));
        }
        listeners.push(controls.modeSelectBox.listen("change", function(event) {
            self.vars.grid.setDisplayMode(event.detail.value);
        }));
        listeners.push(controls.filterCheckBoxError.listen("change", function(event) {
            toggleFilterSet(self, "error");
            updateTableFilter(self);
        }));
        listeners.push(controls.filterCheckBoxWarn.listen("change", function(event) {
            toggleFilterSet(self, "warn");
            updateTableFilter(self);
        }));
        listeners.push(controls.filterCheckBoxAdvise.listen("change", function(event) {
            toggleFilterSet(self, "advise");
            updateTableFilter(self);
        }));
        return true;
    }

    /**
     * Gathers all anchor-elements to append controls
     *
     * @param {object} self Control-Obj
     * @returns {bool}
     */
    function gatherAnchors(self) {
        const anchors = self.vars.anchorEls,
            io = shmi.requires("visuals.io");

        anchors.filterCheckBoxError = shmi.getUiElement('filterCheckBoxError', self.element);
        anchors.filterCheckBoxWarn = shmi.getUiElement('filterCheckBoxWarn', self.element);
        anchors.filterCheckBoxAdvise = shmi.getUiElement('filterCheckBoxAdvise', self.element);
        anchors.filterGroupSelectBox = shmi.getUiElement('filterGroupSelectBox', self.element);
        anchors.modeSelectBox = shmi.getUiElement("mode-select-box", self.element);
        anchors.alarmList = shmi.getUiElement('complex-table2', self.element);
        anchors.confirmButtons = shmi.getUiElement('confirmButtons', self.element);
        anchors.filterButton = shmi.getUiElement("filter-button", self.element);

        if (anchors.filterButton) {
            const filterHandler = {
                    onClick: function() {
                        handleDialog(self);
                    }
                },
                ml = new io.MouseListener(anchors.filterButton, filterHandler),
                tl = new io.TouchListener(anchors.filterButton, filterHandler);
            self.vars.listeners.push(ml, tl);
        }

        return true;
    }

    /**
     * Gathers all anchor-elements to append controls
     *
     * @param {object} self Control-Obj
     * @param {object} anchors anchors object
     * @param {object} configs configuration holder object
     * @returns {bool}
     */
    function configureAlarmList(self, anchors, configs) {
        configs.alarmTableConfig.table = self.vars.dataGridId;

        if (shmi.hasClass(anchors.alarmList, "two-rows")) {
            configs.alarmTableConfig["default-layout"]["line-height"] = "78px";
        }

        if (self.config.columnConfig) {
            configs.alarmTableConfig["default-layout"]["column-org"] = self.config.columnConfig;
        }

        if (self.config["notify-image"]) {
            configs.filterCheckBoxAdvise["icon-src"] = self.config["notify-image"];
            configs.alarmTableConfig["default-field-control-map"].level.config.options[0]["icon-src"] = self.config["notify-image"];
        }
        if (self.config["warn-image"]) {
            configs.filterCheckBoxWarn["icon-src"] = self.config["warn-image"];
            configs.alarmTableConfig["default-field-control-map"].level.config.options[1]["icon-src"] = self.config["warn-image"];
        }
        if (self.config["alarm-image"]) {
            configs.filterCheckBoxError["icon-src"] = self.config["alarm-image"];
            configs.alarmTableConfig["default-field-control-map"].level.config.options[2]["icon-src"] = self.config["alarm-image"];
        }

        if (self.config["detail-template"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["detail-template"] = self.config["detail-template"];
        }

        if (self.config["comment-template"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["comment-template"] = self.config["comment-template"];
        }

        if (self.config["comment-mode"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["comment-mode"] = self.config["comment-mode"];
        }

        if (self.config["dateformat"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["dateformat"] = self.config["dateformat"];
        }

        configs.alarmTableConfig["default-field-control-map"].json.config["inline-svg"] = self.config["inline-svg"] ?? true;

        Object.assign(configs.alarmTableConfig["default-field-control-map"].json.config, {
            "notify-image": self.config["notify-image"],
            "warn-image": self.config["warn-image"],
            "alarm-image": self.config["alarm-image"]
        });

        if (self.vars.detailsRight) {
            //remove details button from CT2 and show empty detail win
            delete (configs.alarmTableConfig["default-layout"]["column-org"].col7);
            const alarmDetailsIQ = shmi.requires("visuals.tools.alarms.ls.alarmDetailsIQ");
            alarmDetailsIQ.showAlarmDetails(null, "inline", self.vars.detailsRight, self);
        }

        if (self.config["disable-commit"]) {
            //remove alarm commit button from CT2
            if (configs.alarmTableConfig["default-layout"]["column-org"].col7) {
                configs.alarmTableConfig["default-layout"]["column-org"].col6 = configs.alarmTableConfig["default-layout"]["column-org"].col7;
                delete (configs.alarmTableConfig["default-layout"]["column-org"].col7);
            } else {
                delete (configs.alarmTableConfig["default-layout"]["column-org"].col6);
            }
        }
    }
    /**
     * Creates Childcontrols
     *
     * @param {object} self Control-Obj
     * @returns {bool}
     */
    function createChildControls(self) {
        const anchors = self.vars.anchorEls,
            controls = self.vars.controls,
            configs = self.vars.configs;

        configureAlarmList(self, anchors, configs);

        controls.filterCheckBoxError = shmi.createControl('iq-checkbox', anchors.filterCheckBoxError, configs.filterCheckBoxError, 'DIV');
        controls.filterCheckBoxWarn = shmi.createControl('iq-checkbox', anchors.filterCheckBoxWarn, configs.filterCheckBoxWarn, 'DIV');
        controls.filterCheckBoxAdvise = shmi.createControl('iq-checkbox', anchors.filterCheckBoxAdvise, configs.filterCheckBoxAdvise, 'DIV');

        if (anchors.filterGroupSelectBox) {
            //clone config to avoid duplication of entries when list of alarm groups is updated
            controls.filterGroupSelectBox = shmi.createControl('iq-select-box', anchors.filterGroupSelectBox, shmi.cloneObject(configs.filterGroupSelectBox), 'DIV');
        }
        controls.modeSelectBox = shmi.createControl('iq-select-box', anchors.modeSelectBox, configs.modeSelectBox, 'DIV');
        controls.createThreadButton = shmi.createControl('iq-button', anchors.confirmButtons, configs.createThreadButton, 'DIV');
        controls.confirmAllAlarmsButton = shmi.createControl('iq-button', anchors.confirmButtons, configs.confirmAllAlarmsButton, 'DIV');

        controls.alarmList = shmi.createControl('complex-table2', anchors.alarmList, configs.alarmTableConfig, 'DIV', "from");

        return true;
    }

    /**
     * update labels of filter time start & end buttons
     *
     * @param {object} state controller state
     */
    function updateTimeLabels(state) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            buttonStart = state.getInstance("selectTimeStart"),
            buttonEnd = state.getInstance("selectTimeEnd");

        buttonStart.setLabel(
            formatDateTime(Math.floor(state.data.timestampStart / 1000), { datestring: shmi.localize(state.data.dateTimeFormat), localize: true })
        );
        buttonEnd.setLabel(
            formatDateTime(Math.floor(state.data.timestampEnd / 1000), { datestring: shmi.localize(state.data.dateTimeFormat), localize: true })
        );
    }

    /**
     * handler for filter button - creates the dialog
     *
     * @param {object} self Control-Obj
     */
    function handleDialog(self) {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (baseContainer) {
            baseContainer.addControl([
                {
                    ui: "dialog-box",
                    controller: {
                        name: "alarmTimeFilterController",
                        slots: {
                            dialog: {
                                ui: "dialog-box",
                                events: ["close"]
                            },
                            selectTimeStart: {
                                ui: "iq-button",
                                events: ["click"]
                            },
                            selectTimeEnd: {
                                ui: "iq-button",
                                events: ["click"]
                            },
                            cancelButton: {
                                ui: "iq-button",
                                events: ["click"]
                            },
                            applyButton: {
                                ui: "iq-button",
                                events: ["click"]
                            }
                        },
                        data: {
                            dateTimeFormat: self.config["dateformat"],
                            timestampStart: Date.now() - 86400000,
                            timestampEnd: Date.now()
                        },
                        onChange: function(state) {
                        },
                        onDisable: function(state) {
                            shmi.deleteControl(state.getParent());
                        },
                        onEnable: function(state) {
                            updateTimeLabels(state);
                        },
                        onEvent: async function(state, slot, type, event) {
                            const { requestDateAndTime } = shmi.requires("visuals.tools.date");

                            //...handle events generated by slot controls
                            switch (slot) {
                            case "selectTimeStart": {
                                const startTime = await requestDateAndTime({
                                    timestamp: state.data.timestampStart
                                });
                                if (startTime !== null) {
                                    state.data.timestampStart = startTime;
                                    updateTimeLabels(state);
                                }
                                break;
                            }
                            case "selectTimeEnd": {
                                const endTime = await requestDateAndTime({
                                    timestamp: state.data.timestampEnd
                                });
                                if (endTime !== null) {
                                    state.data.timestampEnd = endTime;
                                    updateTimeLabels(state);
                                }
                                break;
                            }
                            case "applyButton":
                                setFilter(self, state);
                                state.getInstance("dialog").hide();
                                break;
                            case "cancelButton":
                                state.getInstance("dialog").hide();
                                break;
                            default:
                            }
                        }
                    },
                    config: {
                        "title": "${alarmlist_select_time_range}",
                        "class-name": "dialog-box iq-alarm-list-filter-dialog",
                        "template": "default/dialog-box",
                        "name": "alarmTimeFilterDialog",
                        "top-level": true,
                        "content-template": null,
                        "_controllers_": [
                            {
                                "name": "alarmTimeFilterController",
                                "slot": "dialog"
                            }
                        ]
                    },
                    children: [
                        {
                            ui: "container",
                            config: {
                                "name": "container"
                            },
                            children: [
                                {
                                    ui: "iq-label",
                                    config: {
                                        "class-name": "iq-label",
                                        "text": "${alarmlist_from}"
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-02 date-time-selection",
                                        "template": "default/iq-button.date-time-selection",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "buttonStart",
                                        "ui": "iq-button",
                                        "label": "[Start Date/Time]",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": [
                                            {
                                                "name": "alarmTimeFilterController",
                                                "slot": "selectTimeStart"
                                            }
                                        ]
                                    },
                                    "children": null
                                },
                                {
                                    ui: "iq-label",
                                    config: {
                                        "class-name": "iq-label",
                                        "text": "${alarmlist_to}"
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-02 date-time-selection",
                                        "template": "default/iq-button.date-time-selection",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "buttonEnd",
                                        "ui": "iq-button",
                                        "label": "[End Date/Time]",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": [
                                            {
                                                "name": "alarmTimeFilterController",
                                                "slot": "selectTimeEnd"
                                            }
                                        ]
                                    },
                                    "children": null
                                },
                                {
                                    ui: "container",
                                    config: {
                                        "class-name": "iq-container footer",
                                        "type": "iqflex",
                                        "flex-primary-align": "end",
                                        "flex-orientation": "row",
                                        "flex-distribute": false,
                                        "flex-all": false,
                                        "name": "footerContainer",
                                        "template": null
                                    },
                                    children: [
                                        {
                                            ui: "iq-button",
                                            config: {
                                                "class-name": "iq-button cancel-button",
                                                "label": "${cancel}",
                                                "_controllers_": [
                                                    {
                                                        "name": "alarmTimeFilterController",
                                                        "slot": "cancelButton"
                                                    }
                                                ]
                                            }
                                        },
                                        {
                                            ui: "iq-button",
                                            config: {
                                                "class-name": "iq-button filled apply-button",
                                                "label": "${okay}",
                                                "_controllers_": [
                                                    {
                                                        "name": "alarmTimeFilterController",
                                                        "slot": "applyButton"
                                                    }
                                                ]
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ], function(err, controls) {
                if (err) {
                    console.error("Error creating controls");
                } else {
                    controls[0].show();
                }
            });
        } else {
            console.error("Error creating filter dialog: base container not found in layout");
        }
    }
    /**
     * set time filter
     *
     * @param {object} self Control-Obj
     * @param {object} state controller state
     */
    function setFilter(self, state) {
        const from = Math.floor(state.data.timestampStart / 1000),
            to = Math.floor(state.data.timestampEnd / 1000),
            dt = shmi.requires("visuals.tools.date");

        shmi.addClass(self.element, "filter-active");
        self.vars.activeFilterLabel.textContent = shmi.localize("${alarmlist_timefilter}");
        self.vars.activeFilterFrom.textContent = dt.formatDateTime(from, { datestring: shmi.localize(self.config.dateformat), localize: true });
        self.vars.activeFilterTo.textContent = dt.formatDateTime(to, { datestring: shmi.localize(self.config.dateformat), localize: true });
        self.vars.controls.alarmList.resetScroll();
        self.vars.grid.setFilter(4, [from, to]);
    }
    /**
     * clear time filter
     *
     * @param {object} self Control-Obj
     */
    function clearFilter(self) {
        self.vars.controls.alarmList.resetScroll();
        self.vars.grid.clearFilter(4);
        shmi.removeClass(self.element, "filter-active");
    }
    /**
     * attach listener to selection of rows
     *
     * @param {object} self Control-Obj
     * @param {object} ct complex table reference
     */
    function attachSelectListener(self, ct) {
        const controls = [];

        if (self.vars.controls.createThreadButton) {
            controls.push(self.vars.controls.createThreadButton);
        }

        self.vars.tokens.push(ct.listen("select", function(evt) {
            if (evt.detail.selRows.length === 1) {
                if (self.vars.detailsRight) {
                    const alarmInfoColumn = 8,
                        alarmInfo = JSON.parse(ct.getSelectionRowData(evt.detail)[0][alarmInfoColumn].value),
                        alarmDetailsIQ = shmi.requires("visuals.tools.alarms.ls.alarmDetailsIQ");

                    alarmDetailsIQ.showAlarmDetails(alarmInfo, "inline", self.vars.detailsRight, self);
                }

                controls.forEach((c) => c.unlock());
            } else {
                controls.forEach((c) => c.lock());
            }
        }));
    }

    /**
     * Updates the options of the group filter select-box with available alarm
     * groups returned from the backend.
     *
     * @param {object} self Reference to the iq-alarm-list widget
     */
    function updateGroupFilterSelectbox(self) {
        const { controls: { filterGroupSelectBox }, groupFilter } = self.vars;

        if (!filterGroupSelectBox) {
            return;
        }

        shmi.visuals.session.ConnectSession.requestPromise("alarm.get_groups", {}).then((groups) => {
            const oldValue = filterGroupSelectBox.getValue();
            filterGroupSelectBox.setOptions(
                [
                    ...self.vars.configs.filterGroupSelectBox.options,
                    ...groups.filter((groupId) => !groupFilter || groupFilter.includes(groupId)).map((groupId) => ({
                        label: `\${alarm_group_${groupId}}`,
                        value: groupId
                    }))
                ]
            );
            filterGroupSelectBox.setValue(oldValue);
        }).catch((e) => {
            console.error(`[${className}] Unable to get list of alarm groups`, e);

            filterGroupSelectBox.setOptions(self.vars.configs.filterGroupSelectBox.options);
            filterGroupSelectBox.setValue(null);
        });
    }

    //declare private functions - END

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            tempAlarmGrid: {},
            alarmSubscription: null,
            alarmLevelTemplate: null,
            grid: null,
            dataGridId: "",
            groupFilter: null,
            cancelable: null,
            filterSet: {
                warn: false,
                error: false,
                advise: false
            },
            anchorEls: {
                filterCheckBoxError: null,
                filterCheckBoxWarn: null,
                filterCheckBoxAdvise: null,
                filterGroupSelectBox: null,
                modeSelectBox: null,
                alarmList: null,
                confirmButtons: null,
                filterButton: null,
                filterOverlay: null
            },
            controls: {
                filterCheckBoxError: null,
                filterCheckBoxWarn: null,
                filterCheckBoxAdvise: null,
                filterGroupSelectBox: null,
                modeSelectBox: null,
                alarmList: null,
                confirmAllAlarmsButton: null,
                createThreadButton: null
            },
            configs: {
                alarmTableConfig: {
                    "label": "${alarmlist_history_title}",
                    "table": "iqalarms",
                    "name": "alarm-table",
                    "class-name": "complex-table2 alarms",
                    "template": null,
                    "field-datagrid-col-map": {
                        "level": 3,
                        "id": 1,
                        "description": 9,
                        "come": 4,
                        "group": 10,
                        "isAck": 8,
                        "json": 8
                    },
                    "select-mode": "SINGLE",
                    "default-field-control-map": {
                        "level": {
                            "ui-type": "iq-image-changer",
                            "config": {
                                "class-name": "iq-image-changer iq-variant-01",
                                "template": "default/iq-image-changer.iq-variant-01",
                                "default-image": "",
                                "no-initial-image": true,
                                "options": [
                                    {
                                        "label": "label",
                                        "value": 0,
                                        "icon-src": "pics/system/controls/iq-alarm-list/icon-info.svg",
                                        "icon-title": null,
                                        "icon-class": null,
                                        "style": {
                                            "color": "var(--info-color)"
                                        }
                                    },
                                    {
                                        "label": "label",
                                        "value": 1,
                                        "icon-src": "pics/system/controls/iq-alarm-list/icon-warning.svg",
                                        "icon-title": null,
                                        "icon-class": null,
                                        "style": {
                                            "color": "var(--warning-color)"
                                        }
                                    },
                                    {
                                        "label": "label",
                                        "value": 2,
                                        "icon-src": "pics/system/controls/iq-alarm-list/icon-alarm.svg",
                                        "icon-title": null,
                                        "icon-class": null,
                                        "style": {
                                            "color": "var(--alarm-color)"
                                        }
                                    }
                                ],
                                "scaling-mode": "contain",
                                "inline-svg": true
                            }
                        },
                        "id": {
                            "ui-type": "iq-label",
                            "config": {
                                "class-name": "iq-label",
                                "options": [],
                                "pattern": "<%= VALUE %>",
                                "value-as-tooltip": true
                            }
                        },
                        "description": {
                            "ui-type": "iq-label",
                            "config": {
                                "class-name": "iq-label",
                                "options": [],
                                "pattern": null,
                                "value-as-tooltip": true
                            }
                        },
                        "come": {
                            "ui-type": "iq-date-time",
                            "config": {
                                "class-name": "iq-date-time multiline",
                                "template": "default/iq-date-time.iq-variant-01",
                                "display-format": "$DD.$MM.$YYYY, $HH:$mm:$ss",
                                "value-as-tooltip": true
                            }
                        },
                        "group": {
                            "ui-type": "iq-label",
                            "config": {
                                "class-name": "iq-label",
                                "options": [],
                                "pattern": null,
                                "value-as-tooltip": true
                            }
                        },
                        "isAck": {
                            "ui-type": "local-script",
                            "config": {
                                "module": "visuals.tools.alarms.ls.alarmCommitButton"
                            }
                        },
                        "json": {
                            "ui-type": "local-script",
                            "config": {
                                "module": "visuals.tools.alarms.ls.alarmDetailsIQ",
                                "detail-template": null,
                                "comment-template": null,
                                "dateformat": null
                            }
                        }
                    },
                    "default-field-headers": {
                        "level": "${alarmlist_header_level}",
                        "id": "${alarmlist_header_id}",
                        "description": "${alarmlist_header_description}",
                        "come": "${alarmlist_header_come}",
                        "group": "${alarmlist_header_group}",
                        "isAck": "${alarmlist_header_isack}",
                        "json": "${alarmlist_header_json}"
                    },
                    "_comment": "expr is passed to expr parameter array of shmi.visuals.core.DataGridManager.setFilter",
                    "filters": [],
                    "default-layout": {
                        "class-name": "layout-std",
                        "_comment": "default == no additional css layout class",
                        "column-org": {
                            "col1": {
                                "fields": ["level"],
                                "column-width": "60px"
                            },
                            "col2": {
                                "fields": ["id"],
                                "column-width": "50px"
                            },
                            "col3": {
                                "fields": ["description"]
                            },
                            "col4": {
                                "fields": ["come"],
                                "column-width": "150px"
                            },
                            "col5": {
                                "fields": ["group"],
                                "column-width": "150px"
                            },
                            "col6": {
                                "fields": ["isAck"],
                                "column-width": "100px"
                            },
                            "col7": {
                                "fields": ["json"],
                                "column-width": "90px"
                            }
                        },
                        "line-height": "var(--table-row-height)"
                    },
                    "sortable-fields": [
                        "level",
                        "id",
                        "come",
                        "group"
                    ],
                    "delete-selected-rows": false,
                    "show-nof-rows": true,
                    "show-buttons-table-min-width-px": 400,
                    "text-mode": "MULTILINE",
                    "default-nof-buffered-rows": 60,
                    "buffer-size": 500
                },
                filterCheckBoxError: {
                    'class-name': 'iqel-filter-checkbox',
                    'template': 'default/iq-alarm-list/filter-checkbox',
                    "icon-src": "pics/system/controls/iq-alarm-list/icon-alarm.svg",
                    'label': '${alarmlist_level_errors}',
                    "show-icon": true,
                    "inline-svg": true
                },
                filterCheckBoxWarn: {
                    'class-name': 'iqel-filter-checkbox',
                    'template': 'default/iq-alarm-list/filter-checkbox',
                    "icon-src": "pics/system/controls/iq-alarm-list/icon-warning.svg",
                    'label': '${alarmlist_level_warnings}',
                    "show-icon": true,
                    "inline-svg": true
                },
                filterCheckBoxAdvise: {
                    'class-name': 'iqel-filter-checkbox',
                    'template': 'default/iq-alarm-list/filter-checkbox',
                    "icon-src": "pics/system/controls/iq-alarm-list/icon-info.svg",
                    'label': '${alarmlist_level_notifications}',
                    "show-icon": true,
                    "inline-svg": true
                },
                filterGroupSelectBox: {
                    "name": "filter-group-select-box",
                    'class-name': 'iq-select-box',
                    'show-text': false,
                    'options': [
                        {
                            'label': '${alarmlist_filter_group_all}',
                            'value': null
                        }
                    ]
                },
                modeSelectBox: {
                    'name': "mode-select-box",
                    'class-name': 'iq-select-box',
                    'show-text': false,
                    'options': [{
                        'label': '${alarmlist_mode_all}',
                        'value': 'all'
                    },
                    {
                        'label': '${alarmlist_mode_live}',
                        'value': 'live'
                    },
                    {
                        'label': '${alarmlist_mode_historic}',
                        'value': 'historic'
                    }]
                },
                alarmList: {},
                confirmAllAlarmsButton: {
                    'label': '${alarmlist_commit_all}',
                    'name': 'confirm-all-al-btn',
                    'class-name': 'iq-button filled ack-all'
                },
                createThreadButton: {
                    'label': '${alarmlist_create_thread}',
                    'name': 'create-thread-btn',
                    'class-name': 'iq-button iq-variant-01 outlined create-thread'
                }
            },
            listeners: [],
            tokens: [],
            editListeners: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dgm: 'visuals.session.DataGridManager',
            am: 'visuals.session.AlarmManager'
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                if (!controlStartUp(self)) {
                    shmi.log('Init Failed!');
                } else {
                    shmi.log('Init Succeeded!');
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.enable());

                attachSelectListener(self, self.vars.controls.alarmList);

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].enable) {
                        self.vars.controls[key].enable();
                    }
                }

                if (self.vars.controls.createThreadButton) {
                    self.vars.controls.createThreadButton.lock();
                }

                self.vars.cancelable = shmi.onReady({
                    controls: {
                        modeSelectBox: self.getName() + ".mode-select-box",
                        filterGroupSelectBox: self.getName() + ".filter-group-select-box"
                    }
                }, function(ref) {
                    ref.controls.modeSelectBox.setValue(self.config["display-mode"]);
                    ref.controls.filterGroupSelectBox.setValue(null);
                    updateGroupFilterSelectbox(self);
                    setListener(self);
                });

                shmi.log("[IQ:iq-alarm-list] Enabled", 1);
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.disable());

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].disable) {
                        self.vars.controls[key].disable();
                    }
                }

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];

                self.vars.editListeners.forEach((l) => l.disable());
                self.vars.editListeners = [];

                if (self.vars.cancelable) {
                    self.vars.cancelable.cancel();
                    self.vars.cancelable = null;
                }

                shmi.log("[IQ:iq-alarm-list] disabled", 1);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.disable());

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].lock) {
                        self.vars.controls[key].lock();
                    }
                }

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-alarm-list] Locked", 1);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.enable());

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].unlock) {
                        self.vars.controls[key].unlock();
                    }
                }

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-alarm-list] unlocked", 1);
            },
            onDelete: function() {
                const self = this;

                delete self.imports.dgm.grids[self.vars.dataGridId];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

(function() {
    /**
     * module to display alarm-detail messages in alarm-table.
     */

    const MODULE_NAME = "visuals.tools.alarms.ls.alarmCommitButton",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @author Matthias Weber <weber@smart-hmi.de>
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const im = shmi.requires("visuals.session.ItemManager"),
            am = shmi.requires("visuals.session.AlarmManager"),
            itemHandler = im.getItemHandler();
        let alarmInfo = null,
            ackButton = null,
            tokens = [];

        self.vars = self.vars || {};
        itemHandler.setValue = function(value) {
            alarmInfo = JSON.parse(value);
            if (ackButton) shmi.deleteControl(ackButton, true);

            let currentElement = self.element;
            while (currentElement && currentElement.tagName !== "TR") {
                currentElement = currentElement.parentElement;
            }

            if (currentElement) {
                if (alarmInfo.active) {
                    shmi.addClass(currentElement, "active-alarm");
                } else {
                    shmi.removeClass(currentElement, "active-alarm");
                }
            }

            if (alarmInfo.acknowledgeable) {
                if (alarmInfo.acknowledged) {
                    ackButton = shmi.createControl("iq-button", self.element.parentNode, { "class-name": "iq-button iq-variant-01 filled", "icon-src": "pics/system/controls/iq-alarm-list/icon-confirm-single.svg", "label": "", "show-icon": true, "show-text": false, "inline-svg-icon": self.config["inline-svg"] ?? true }, "DIV");
                    shmi.addClass(ackButton.element, "alarm-ack");
                } else {
                    ackButton = shmi.createControl("iq-button", self.element.parentNode, { "class-name": "iq-button iq-variant-01 filled", "icon-src": "pics/system/controls/iq-alarm-list/icon-confirm-single.svg", "label": "", "show-icon": true, "show-text": false, "inline-svg-icon": self.config["inline-svg"] ?? true }, "DIV");
                    shmi.addClass(ackButton.element, "alarm-ack");
                    tokens.push(ackButton.listen("click", function() {
                        // ACK ALARM
                        if (alarmInfo.real_id) {
                            am.ackAlarm(alarmInfo.real_id);
                        }
                    }));
                }
                if (!alarmInfo.active || alarmInfo.acknowledged) {
                    ackButton.lock();
                }
            } else {
                ackButton = shmi.createControl("iq-label", self.element.parentNode, { "text": "---" }, "DIV");
                shmi.addClass(ackButton.element, "alarm-no-ack");
            }
        };

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }
        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
            if (ackButton) shmi.deleteControl(ackButton, true);
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();

(function() {
    /**
     * module to display alarm-detail messages in alarm-table.
     */

    const MODULE_NAME = "visuals.tools.alarms.ls.alarmDetailsIQ",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * format time to display
     *
     * @param {number} timestamp timestamp
     * @param {string} dateformat date format string
     *
     * @returns {string} formatted date
     */
    function formatTime(timestamp, dateformat) {
        const { formatDateTime } = shmi.requires("visuals.tools.date");

        if (timestamp === null) {
            return "---";
        } else {
            return formatDateTime(timestamp, { datestring: dateformat ? shmi.localize(dateformat) : null, localize: true });
        }
    }
    /**
     * replace zero with '---''
     *
     * @param {Number} value
     */
    function zeroOutNull(value) {
        if (value === null) {
            return "---";
        } else {
            return value;
        }
    }

    /**
     * Opens the create thread dialog box for the given alarm.
     *
     * @param {number} referenceId
     * @param {"alarm"|"alarm_type"} referenceType
     * @returns {Promise<number|null>}
     */
    function openCreateThreadDialogForAlarm(referenceId, referenceType) {
        const { create: createThread } = shmi.requires("visuals.tools.threads");

        return createThread(referenceType, referenceId, null, {
            dialogTitle: "${alarmlist_create_thread}",
            titleLabel: "${alarmlist_threads_title}",
            messageLabel: "${alarmlist_threads_message}",
            applyLabel: "${alarmlist_threads_create_btn}"
        });
    }

    /**
     * Opens the create thread dialog box for the given alarm info according to
     * the widget configuration.
     *
     * @param {*} self
     * @param {object} alarmInfo
     * @returns {Promise<number|null>}
     */
    function openCreateThreadDialogForAlarmInfo(self, alarmInfo) {
        switch (self.config["comment-mode"]) {
        case "alarm":
            return openCreateThreadDialogForAlarm(alarmInfo.id, "alarm");
        case "alarm_type":
            return openCreateThreadDialogForAlarm(alarmInfo.index, "alarm_type");
        case "disabled":
        default:
            return Promise.resolve(null);
        }
    }

    /**
     * Checks whether comments are enabled or not.
     *
     * @param {*} self
     * @returns {boolean}
     */
    function isCommentModeEnabled(self) {
        return self.config["comment-mode"] === "alarm" || self.config["comment-mode"] === "alarm_type";
    }

    /**
     * @param {*} self
     * @param {HTMLTemplateElement} contentTemplate
     * @param {object} alarmInfo
     */
    function openAlarmDetailsPopup(self, contentTemplate, alarmInfo) {
        const dialogBox = shmi.createControl("dialog-box", self.element.parentNode, {
            "name": "alarmDetailsPopup",
            "class-name": `dialog-box iq-alarm-detail-dialog ${getAlarmSeveritySelector(alarmInfo.severity)}`,
            "top-level": true,
            "title": "${alarmlist_popup_title}"
        }, "DIV");

        let tokens = [];
        const onDialogBoxActive = () => {
            const container = shmi.createControl("container", dialogBox.contentElement, {
                    "class-name": "iq-container content-container"
                }, "DIV"),
                buttonContainer = shmi.createControl("container", dialogBox.contentElement, {
                    "class-name": "iq-container button-footer",
                    "type": "iqflex",
                    "flex-orientation": "row",
                    "flex-primary-align": "end"
                }, "DIV"),
                createThreadButton = isCommentModeEnabled(self) ? shmi.createControl("iq-button", buttonContainer.marginCompensator, {
                    "label": "${alarmlist_create_thread}"
                }, "DIV") : null,
                closeBtn = shmi.createControl("iq-button", buttonContainer.marginCompensator, {
                    "class-name": "iq-button iq-variant-01 filled",
                    "label": "${close}"
                }, "DIV"),
                targetContainer = container.marginCompensator || container.element;

            tokens.push(
                closeBtn.listen("click", () => dialogBox.hide()),
                dialogBox.listen("close", () => {
                    tokens.forEach((token) => token.unlisten());
                    tokens = [];

                    shmi.deleteControl(dialogBox);
                })
            );

            if (createThreadButton) {
                tokens.push(createThreadButton.listen("click", async () => {
                    const newThreadId = await openCreateThreadDialogForAlarmInfo(self, alarmInfo);
                    if (newThreadId !== null) {
                        dialogBox.hide();
                        showAlarmDetails(alarmInfo, "popup", null, self);
                    }
                }));
            }

            const detailContent = contentTemplate.content.cloneNode(true);

            targetContainer.appendChild(detailContent);
            initCommentEditing(self, targetContainer, dialogBox, alarmInfo);
            dialogBox.show();
        };

        if (dialogBox.isActive()) {
            onDialogBoxActive();
        } else {
            tokens.push(dialogBox.listen("enable", onDialogBoxActive));
        }
    }

    /**
     * Loads a HTML template from a url and returns it as template-tag.
     *
     * @param {string} url Url to the HTML template.
     * @param {?object} [placeholders]
     * @param {object} [loadOptions] Options to pass to the resource loader.
     * @returns {Promise<HTMLTemplateElement>}
     */
    async function loadHTMLTemplate(url, placeholders = null, loadOptions = {}) {
        const templateData = await shmi.loadResourcePromise(url, loadOptions),
            template = document.createElement("template");

        if (placeholders) {
            template.innerHTML = shmi.localize(shmi.evalString(templateData, placeholders), placeholders);
        } else {
            template.innerHTML = shmi.localize(templateData);
        }

        return template;
    }

    /**
     * Loads comments for the given alarm using either the alarm id or alarm
     * index depending on the widget configuration.
     *
     * @param {object} self Control-Obj
     * @param {object?} alarmInfo Alarm info
     * @returns {Promise<object[]>}
     */
    async function loadCommentsForAlarm(self, alarmInfo) {
        const { list } = shmi.requires("visuals.tools.threads");

        if (!alarmInfo) {
            return Promise.resolve([]);
        }

        try {
            switch (self.config["comment-mode"]) {
            case "alarm":
                return (await list("alarm", alarmInfo.id, {
                    loadReplies: true,
                    loadUserDisplayName: true
                })).threads;
            case "alarm_type":
                return (await list("alarm_type", alarmInfo.index, {
                    loadReplies: true,
                    loadUserDisplayName: true
                })).threads;
            case "disabled":
            default:
            }
        } catch (e) {
            console.error(`[${MODULE_NAME}]`, "Failed to load comments:", e);
        }

        return [];
    }

    /**
     * initialize thread-edit element to edit threads created by current user
     *
     * @param {object} self alarmlist or local-script instance depending on alarm-detail mode
     * @param {HTMLElement} element UI element to trigger thread editing
     * @param {object} [dialog=null] dialog instance in case of "popup" alarm-detail-mode
     * @param {object} [alarmInfo=null] alarm info data
     */
    function initEditElement(self, element, dialog = null, alarmInfo = null) {
        const userId = element.getAttribute("data-user-id"),
            threadId = parseInt(element.getAttribute("data-thread-id")),
            um = shmi.requires("visuals.session.UserManager"),
            { MouseListener, TouchListener } = shmi.requires("visuals.io"),
            { currentUser } = um;
        if (!isNaN(threadId) && currentUser && userId === currentUser.id) {
            const { edit } = shmi.requires("visuals.tools.threads"),
                handler = {
                    onClick: async () => {
                        const new_threadId = await edit(threadId, {
                            dialogTitle: "${alarmlist_edit_thread}",
                            titleLabel: "${alarmlist_threads_title}",
                            messageLabel: "${alarmlist_threads_message}",
                            applyLabel: "${alarmlist_threads_edit_btn}",
                            deleteLabel: "${alarmlist_threads_delete_btn}"
                        });
                        if (new_threadId !== null) {
                            if (dialog) {
                                dialog.hide();
                                showAlarmDetails(alarmInfo, "popup", null, self);
                            } else if (self.vars.detailsRight) {
                                showAlarmDetails(alarmInfo, "inline", self.vars.detailsRight, self);
                            }
                        }
                    }
                },
                ml = new MouseListener(element, handler),
                tl = new TouchListener(element, handler);

            ml.enable();
            tl.enable();
            self.vars.editListeners.push(ml, tl);
            shmi.addClass(element, "editable");
        }
    }

    /**
     * initialize thread-edit capabilities
     *
     * @param {object} self alarmlist or local-script instance depending on alarm-detail mode
     * @param {HTMLElement} detailContent parent element of alarm-detail content
     * @param {object} [dialog=null] dialog instance in case of "popup" alarm-detail-mode
     * @param {object} [alarmInfo=null] alarm info data
     */
    async function initCommentEditing(self, detailContent, dialog = null, alarmInfo = null) {
        const session = shmi.requires("visuals.session"),
            request = session.ConnectSession.requestPromise.bind(session.ConnectSession);

        let editElements = [],
            mayEdit = false;

        //workaround - test if user may edit thread
        try {
            await request("thread.modify", { thread_id: -1, title: null, message: "" });
        } catch (err) {
            if (!(err.category === "shmi:connect:api:generic" && err.errc === 4)) {
                mayEdit = true;
            }
        }

        self.vars.editListeners.forEach((l) => {
            l.disable();
        });
        self.vars.editListeners = [];

        if (mayEdit) {
            editElements = [...detailContent.querySelectorAll("[data-ui=thread-edit]")];
            editElements.forEach((e) => {
                initEditElement(self, e, dialog, alarmInfo);
            });
        }
    }

    function getAlarmSeveritySelector(severity) {
        switch (severity) {
        case 0:
            return "severity-info";
        case 1:
            return "severity-warning";
        case 2:
            return "severity-alarm";
        default:
            return "severity-none";
        }
    }

    /**
     * show the alarm details
     *
     * @param {object} alarmInfo alarm info object
     * @param {string} mode display mode for alarm details
     * @param {object} target target element
     * @param {object} self Control-Obj
     */
    async function showAlarmDetails(alarmInfo, mode, target, self) {
        const { renderThreadList } = shmi.requires("visuals.tools.threads.render");
        let templateLocation = "templates/default/iq-alarm-list/detail-win.html";

        const threadRenderOptions = {
            renderReplies: true
        };

        if (self.config["detail-template"]) {
            templateLocation = `templates/${self.config["detail-template"]}.html`;
        }

        if (self.config["comment-template"]) {
            threadRenderOptions.templateUrl = self.config["comment-template"];
        }

        function getSeverityAlarmIconSrc(severity) {
            switch (severity) {
            case 0:
                return self.config["notify-image"];
            case 1:
                return self.config["warn-image"];
            case 2:
                return self.config["alarm-image"];
            default:
                return "";
            }
        }

        let alarmObj = {
            ALARM_SEVERITY: -1,
            ALARM_SEVERITY_ICON: "",
            ALARM_SEVERITY_ICON_SRC: "",
            ALARM_SEVERITY_SELECTOR: "severity-none",
            ALARM_COME: "",
            ALARM_GONE: "",
            ALARM_ACK: "",
            ALARM_ACKBY: "",
            ALARM_INDEX: "",
            ALARM_TITLE: "",
            ALARM_URLS: "",
            ALARM_URLS_MATCHED: false,
            ALARM_TEXT: ""
        };

        if (alarmInfo) {
            const urlAttributes = Object.entries(alarmInfo.attributes).filter(([key, value]) => typeof value === "string" && value.startsWith("[URL]")),
                imgAttributes = Object.entries(alarmInfo.attributes).filter(([key, value]) => typeof value === "string" && value.startsWith("[IMG]")),
                alarmUrls = urlAttributes.map(([key, value]) => `${key}: <a href='${value.substr(5)}' target='_blank'>${value.substr(5)}</a><br />`).join(""),
                alarmImages = {},
                dateformat = self.config.dateformat ? shmi.localize(self.config.dateformat) : null;

            imgAttributes.forEach(([key, value]) => {
                alarmImages[key] = `<img class="iqel-alarm-image" src="${value.replace("[IMG]", "").trim()}" />`;
            });

            alarmObj = {
                ALARM_SEVERITY: alarmInfo.severity,
                ALARM_SEVERITY_ICON: "",
                ALARM_SEVERITY_ICON_SRC: getSeverityAlarmIconSrc(alarmInfo.severity),
                ALARM_SEVERITY_SELECTOR: getAlarmSeveritySelector(alarmInfo.severity),
                ALARM_COME: formatTime(alarmInfo.timestamp_in, dateformat),
                ALARM_GONE: formatTime(alarmInfo.timestamp_out, dateformat),
                ALARM_ACK: formatTime(alarmInfo.timestamp_acknowledged, dateformat),
                ALARM_ACKBY: zeroOutNull(alarmInfo.acknowledged_by),
                ALARM_INDEX: alarmInfo.index,
                ALARM_TITLE: shmi.evalString(shmi.localize("${alarm_title_" + alarmInfo.index + "}"), alarmInfo),
                ALARM_URLS: alarmUrls,
                ALARM_IMAGES: alarmImages,
                ALARM_URLS_MATCHED: urlAttributes.length > 0,
                ALARM_TEXT: shmi.evalString(shmi.localize("${alarm_msg_" + alarmInfo.index + "}"), alarmInfo)
            };
        }

        let iconSet = false;
        if (self.config["inline-svg"] && alarmObj.ALARM_SEVERITY_ICON_SRC?.toLocaleLowerCase().endsWith(".svg")) {
            try {
                const svgHolder = document.createElement("div");
                svgHolder.innerHTML = await shmi.loadResourcePromise(alarmObj.ALARM_SEVERITY_ICON_SRC);
                const svg = svgHolder.querySelector("svg");
                if (svg) {
                    svg.classList.add("iqel-alarm-icon");
                    alarmObj.ALARM_SEVERITY_ICON = svg.outerHTML;
                    iconSet = true;
                }
            } catch (exc) {
                console.error("Error loading image source:", exc);
            }
        }

        const [template, threads] = await Promise.all([
            loadHTMLTemplate(templateLocation, alarmObj),
            loadCommentsForAlarm(self, alarmInfo)
        ]);

        if (!iconSet) {
            alarmObj.ALARM_SEVERITY_ICON = `<img class="iqel-alarm-icon" src="${alarmObj.ALARM_SEVERITY_ICON_SRC}" />`;
        }

        const commentElement = shmi.getUiElement("comments", template.content);
        if (commentElement) {
            commentElement.append(await renderThreadList(threads, threadRenderOptions));
            if (threads.length) {
                shmi.addClass(commentElement, "has-comments");
            }
        }

        if (!alarmObj.ALARM_URLS_MATCHED) {
            const linkContainer = shmi.getElementsByAttribute("class", "iqel-link-container", template.content);
            if (linkContainer[0]) {
                linkContainer[0].style.display = "none";
            }
        }

        if (mode === "popup") {
            openAlarmDetailsPopup(self, template, alarmInfo);
        } else {
            // Clear detail window.
            while (target.lastChild) {
                target.removeChild(target.lastChild);
            }
            const detailContent = template.content.cloneNode(true);
            target.appendChild(detailContent);
            initCommentEditing(self, target, null, alarmInfo);
        }
    }

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const im = shmi.requires("visuals.session.ItemManager"),
            itemHandler = im.getItemHandler();
        let alarmInfo = null,
            tokens = [];

        self.vars = self.vars || {};
        self.vars.editListeners = [];

        const detailsButton = shmi.createControl("iq-button", self.element.parentNode, { "class-name": "iq-button iq-variant-01 outlined", "icon-src": "pics/system/controls/iq-alarm-list/icon-details.svg", "label": "", "show-icon": true, "show-text": false, "inline-svg-icon": true, "tooltip": "${alarmlist_show_detail}" }, "DIV");
        itemHandler.setValue = function(value) {
            try {
                alarmInfo = JSON.parse(value);
            } catch (e) {
                console.error("Error parsing alarmInfo data");
            }
        };

        tokens.push(detailsButton.listen("click", function() {
            showAlarmDetails(alarmInfo, "popup", null, self);
        }));

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }

        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
            self.vars.editListeners.forEach((l) => {
                l.disable();
            });
            self.vars.editListeners = [];
            shmi.deleteControl(detailsButton, true);
        };
    };
    /**
     * @function
     * show the alarm details
     *
     * @param {object} alarmInfo alarm info object
     * @param {string} mode display mode for alarm details
     * @param {object} target target element
     */
    module.showAlarmDetails = showAlarmDetails;

    module.openCreateThreadDialogForAlarmInfo = openCreateThreadDialogForAlarmInfo;

    // MODULE CODE - END

    fLog("module loaded");
})();

/**
 * WIQ Rocker Button
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-button-rocker",
 *     "name": null,
 *     "template": "custom/controls/iq-button-rocker"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "step": Step width
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-unit-text": Whether to use the auto-unit (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-step": Wheter to use auto-step (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "numpad-enabled": Whether the numpad is enabled
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "inline-svg-icon": Whether to show the icon inlined
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-button-rocker", // control name in camel-case
        uiType = "iq-button-rocker", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-button-rocker iq-variant-01",
        "name": null,
        "template": "default/iq-button-rocker.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "step": 1,
        "precision": -1,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "type": shmi.c("TYPE_INT"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "auto-min": true,
        "auto-max": true,
        "auto-step": true,
        "auto-precision": true,
        "auto-type": true,
        "numpad-enabled": false,
        "show-icon": true,
        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": true,
        "tooltip": null,
        "auto-repeat": false,
        "auto-repeat-interval": 250
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * Checks if all doms elements could be found.
     *
     * @param {object} doms `this.doms`
     * @returns {boolean} `true` if the object did not contain any `null`
     *  values, `false` else.
     */
    function verifyDomElements(doms) {
        const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
        let ok = true;

        iterObj(doms, function(val) {
            ok = ok && val !== null;
        });

        return ok;
    }

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.domChild.unitEl) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined") {
            self.vars.unit = "";
            self.vars.domChild.unitEl.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.domChild.unitEl.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.domChild.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.domChild.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.domChild.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * getButtonStepping - get step interval for increment/decrement buttons. Defaults to 1 when stepping is unset or <=0
     *
     * @param {object} self control instance
     * @returns {number} increment/decrement stepping
     */
    function getButtonStepping(self) {
        if (self.vars.valueSettings && typeof self.vars.valueSettings.step === "number" && self.vars.valueSettings.step > 0) {
            return self.vars.valueSettings.step;
        }
        return 1;
    }

    /**
     * getValueSettings - retrieve active value-settings with fallback to configuration when not available
     *
     * @param {object} self control instance
     * @returns {object} value settings
     */
    function getValueSettings(self) {
        const config = self.getConfig();

        //remove step setting from value settings to allow input independent of button stepping
        const inputSettings = {
            vars: {
                valueSettings: self.vars.valueSettings ? shmi.cloneObject(self.vars.valueSettings) : config
            },
            config: config
        };
        inputSettings.vars.valueSettings.step = 0;

        return inputSettings;
    }

    /**
     * incValue - increment current value by specified amount
     *
     * @param {object} self control instance
     * @param {number} increment increment
     */
    function incValue(self, increment) {
        if (!self.vars.valueSettings) {
            self.imports.nv.initValueSettings(self);
        }
        self.vars.domChild.currentValueEl.blur();
        self.vars.value = self.imports.nv.applyInputSettings(Number(self.vars.value) + increment, getValueSettings(self));
        self.updateValue();
        if (typeof self.vars.value !== 'undefined') {
            self.setCurrentElementValue();
        }
    }

    /**
     * Attaches a DOM listener to an HTML element and adapts it to the visuals
     * listener interface.
     *
     * @param {HTMLElement} element Element to attach the listener to.
     * @param {string} eventName Name of the event to listen for.
     * @param {function} handlerFunc Handler for the event.
     * @returns {{ enable: function; disable: function }}
     */
    function makeDomListener(element, eventName, handlerFunc) {
        return {
            enable: () => element.addEventListener(eventName, handlerFunc, { passive: true }),
            disable: () => element.removeEventListener(eventName, handlerFunc)
        };
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            domChild: {
                // DOM Elements
                prevEl: null,
                nextEl: null,
                currentValueEl: null,
                labelEl: null,
                unitEl: null,
                iconEl: null
            },

            label: null,
            isInputTag: null,
            valueSettings: null,
            buttonInterval: 0
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons"),
                    appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.domChild.prevEl = shmi.getUiElement('previous', self.element);
                self.vars.domChild.nextEl = shmi.getUiElement('next', self.element);
                self.vars.domChild.currentValueEl = shmi.getUiElement('current', self.element);
                self.vars.domChild.labelEl = shmi.getUiElement('label', self.element);
                self.vars.domChild.unitEl = shmi.getUiElement('unit', self.element);
                self.vars.domChild.iconEl = shmi.getUiElement('icon', self.element);

                if (!verifyDomElements(self.vars.domChild)) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                if (self.vars.domChild.currentValueEl) {
                    self.vars.isInputTag = self.vars.domChild.currentValueEl.tagName === 'INPUT';
                }

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.domChild.iconEl) {
                    shmi.log('[IQ:iq-button-rocker] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.domChild.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.domChild.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Label is optional, but prev, next and current are req'd for all variants
                if (self.vars.domChild.prevEl === null || self.vars.domChild.nextEl === null || self.vars.domChild.currentValueEl === null) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-button-rocker] One of the elements is missing (previous, next, current)", 3);
                    self.vars.domChild.prevEl = null;
                    self.vars.domChild.nextEl = null;
                    self.vars.domChild.currentValueEl = null;
                    self.vars.domChild.labelEl = null;
                    self.vars.domChild.unitEl = null;
                    self.vars.domChild.iconEl = null;
                    return;
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);

                // Label
                setLabelImpl(self, self.config.label);

                // Current value
                if (typeof self.vars.value !== 'undefined') {
                    self.setCurrentElementValue();
                }
                self.createInputField(self.vars.domChild.currentValueEl, self.validate.bind(self));

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                let intervalTriggered = false;

                // PREV BUTTON
                const prevFuncs = {
                    onPress: function(x, y, event) {
                        shmi.addClass(self.vars.domChild.prevEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                            intervalTriggered = false;
                            self.vars.buttonInterval = setInterval(() => {
                                intervalTriggered = true;
                                incValue(self, -getButtonStepping(self));
                            }, self.config["auto-repeat-interval"]);
                        }
                    },
                    onRelease: function() {
                        shmi.removeClass(self.vars.domChild.prevEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                        }
                    },
                    onClick: function() {
                        if (!intervalTriggered) {
                            incValue(self, -getButtonStepping(self));
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.domChild.prevEl, prevFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.domChild.prevEl, prevFuncs));

                // NEXT BUTTON
                const nextFuncs = {
                    onPress: function(x, y, event) {
                        shmi.addClass(self.vars.domChild.nextEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                            intervalTriggered = false;
                            self.vars.buttonInterval = setInterval(() => {
                                intervalTriggered = true;
                                incValue(self, getButtonStepping(self));
                            }, self.config["auto-repeat-interval"]);
                        }
                    },
                    onRelease: function() {
                        shmi.removeClass(self.vars.domChild.nextEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                        }
                    },
                    onClick: function() {
                        if (!intervalTriggered) {
                            incValue(self, getButtonStepping(self));
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.domChild.nextEl, nextFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.domChild.nextEl, nextFuncs));

                // KEYDOWN
                // "Validation light" for delimiters.
                self.vars.domChild.currentValueEl.addEventListener('keydown', function(evt) {
                    const itemType = (self.config.item && self.config['auto-type']) ? self.vars.valueSettings.type : self.config.type;
                    if (itemType === shmi.c("TYPE_FLOAT")) {
                        const delimiter = shmi.localize("${DECIMAL_DELIMITER}"),
                            pressedKey = evt.key;
                        if (!pressedKey.match(/[0-9]|Backspace|Delete|Enter|Home|End|ArrowLeft|ArrowRight|ArrowUp|ArrowDown/)) {
                            if (pressedKey !== delimiter) {
                                evt.preventDefault();
                            }
                        }
                    }
                });

                if (self.config["numpad-enabled"] || keyboardEnabled) {
                    self.vars.domChild.currentValueEl?.setAttribute("inputmode", "none");
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-button-rocker] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                clearInterval(self.vars.buttonInterval);
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-button-rocker] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                clearInterval(self.vars.buttonInterval);

                if (self.vars.domChild.currentValueEl === null) return;
                self.vars.domChild.currentValueEl.blur();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(this.element, 'locked');

                if (self.vars.isInputTag) {
                    self.vars.domChild.currentValueEl.disabled = true;
                }

                shmi.log("[IQ:iq-button-rocker] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                if (self.vars.isInputTag) {
                    self.vars.domChild.currentValueEl.disabled = false;
                }

                shmi.log("[IQ:iq-button-rocker] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this;
                self.vars.value = value;
                self.setCurrentElementValue();
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;
                return self.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {
                const self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            setUnitText: function(unitText) {
                const self = this;
                if (self.vars.domChild.unitEl && self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.vars.domChild.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Creates the input field for the IQ Rocker Button
             *
             * @param element - base element of input field
             * @param validateFunc - function to use for validation
             */
            createInputField: function(element, validateFunc) {
                const self = this;

                // VALIDATE: BASE ELEMENT
                if (!element) {
                    shmi.log('[IQ:iq-button-rocker] No base element provided', 3);
                    return;
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                // All required elements found
                function clickHandler() {
                    const appConfig = shmi.requires("visuals.session.config"),
                        keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                    if (self.config["numpad-enabled"] || keyboardEnabled) {
                        const im = shmi.visuals.session.ItemManager,
                            nv = shmi.requires("visuals.tools.numericValues"),
                            params = {
                                "decimal-delimiter": self.config["decimal-delimiter"],
                                "unit": (self.vars.unit !== undefined) ? self.vars.unit : self.config["unit-text"],
                                "label": (self.vars.label !== undefined) ? self.vars.label : self.config.label,
                                "value": self.vars.value,
                                "callback": function(val) {
                                    self.vars.value = val;
                                    self.updateValue();
                                    self.setCurrentElementValue();
                                }
                            };

                        if (!(self.vars && self.vars.valueSettings)) {
                            nv.initValueSettings(self);
                        }

                        const vs = self.vars.valueSettings;
                        params.min = vs.min;
                        params.max = vs.max;
                        params.type = vs.type;
                        params.precision = vs.precision;
                        params.item = (typeof self.config.item === "string" && self.config.item.length > 0) ? self.config.item : null;

                        if (self.config.item && im.getItem(self.config.item) && self.config['auto-type']) {
                            self.type = im.items[self.config.item].type;
                        }

                        shmi.numpad(params);
                        return;
                    }

                    // Auto-select all text on click
                    shmi.addClass(element, 'selectableText');
                    if (!self.vars.isInputTag) {
                        element.setAttribute('contenteditable', true);

                        const range = document.createRange();
                        element.focus();
                        if (element.firstChild && (element.firstChild instanceof Text)) {
                            range.setStart(element.firstChild, 0);
                            range.setEnd(element.firstChild, element.firstChild.length);
                            window.getSelection().removeAllRanges();
                            window.getSelection().addRange(range);
                        } else {
                            shmi.log("[IQ:iq-button-rocker] Element not found", 0);
                        }
                    } else {
                        self.vars.domChild.currentValueEl.setSelectionRange(0, self.vars.domChild.currentValueEl.value.length);
                    }
                }

                self.vars.listeners.push(makeDomListener(element, "click", clickHandler));

                element.addEventListener('keypress', function(event) {
                    if (event.keyCode === 13) {
                        event.preventDefault();
                        window.getSelection().removeAllRanges();
                        element.blur();
                    }
                });

                element.addEventListener('blur', function() {
                    if (!self.vars.isInputTag) {
                        element.setAttribute('contenteditable', false);
                    }
                    window.getSelection().removeAllRanges();
                    shmi.removeClass(element, 'selectableText');
                    shmi.log("[IQ:iq-button-rocker] Blur event", 0);
                    validateFunc(element);
                });
            },

            /**
             * Validation
             *
             * @param element - element to validate the content of
             */
            validate: function(element) {
                const self = this,
                    type = (self.config.item && self.config['auto-type']) ? self.vars.valueSettings.type : self.config.type;
                let exp = null,
                    val = null;

                if (self.vars.isInputTag) {
                    val = self.vars.domChild.currentValueEl.value;
                } else {
                    val = self.vars.domChild.currentValueEl.textContent;
                }

                const inputString = String(val).replace(shmi.localize("${DECIMAL_DELIMITER}"), ".");

                if ([shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT"), shmi.c("TYPE_BOOL")].indexOf(type) !== -1) {
                    exp = new RegExp(self.floatRegexp);
                } else {
                    shmi.log("[IQ:iq-button-rocker] Invalid value type '" + self.config.type + "' configured", 3);
                    self.setCurrentElementValue();
                    return;
                }
                if (exp.test(inputString)) {
                    shmi.log("[IQ:iq-button-rocker] Valid value", 1);
                    self.vars.value = self.imports.nv.applyInputSettings(inputString, getValueSettings(self));
                    self.updateValue();
                } else {
                    shmi.log("[IQ:iq-button-rocker] Invalid value", 1);
                }

                self.setCurrentElementValue();
            },

            /**
             * Writes current value to connected data source
             */
            updateValue: function() {
                const self = this;
                self.fire("change", { value: self.vars.value });
                if (self.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(self.config.item, self.vars.value);
                }
            },

            /**
             * Sets the element value
             */
            setCurrentElementValue: function() {
                const self = this;
                if (self.vars.isInputTag) {
                    self.vars.domChild.currentValueEl.value = self.imports.nv.formatOutput(self.vars.value, self);
                } else {
                    self.vars.domChild.currentValueEl.textContent = self.imports.nv.formatOutput(self.vars.value, self);
                }
            },
            floatRegexp: "(^[+-]?[0-9]([.][0-9]*)?$|^[+-]?[1-9]+[0-9]*([.][0-9]*)?$)"
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-button-toggle
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-button-toggle",
 *     "name": null,
 *     "template": "default/iq-button-toggle.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "tooltip": Tooltip
 * "on-value": ON value (Item)
 * "off-value": OFF value (Item)
 * "confirm-on": Whether to confirm switching it on
 * "confirm-off": Whether to confirm switching it off
 * "on-label": Label for ON
 * "off-label": Label for OFF
 * "on-icon-src": Icon source for ON
 * "on-icon-class": Icon class for ON
 * "on-icon-title": Icon title for ON
 * "off-icon-src": Icon source for OFF
 * "off-icon-class": Icon class for OFF
 * "off-icon-title": Icon title for OFF
 * "confirm-on-text": ON confirmation question,
 * "confirm-off-text": OFF confirmation question
 * "show-icon": Whether to show an icon
 * "show-text": Whether to show text
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "inline-svg-icon": Whether to show the icon inlined
 * "on-action": ON UI actions
 * "off-action": OFF UI actions
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-button-toggle", // control name in camel-case
        uiType = "iq-button-toggle", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-button-toggle iq-variant-01",
        "name": null,
        "template": "default/iq-button-toggle.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "tooltip": null,
        "on-value": 1,
        "off-value": 0,
        "confirm-on": false,
        "confirm-off": false,
        "on-label": "ON",
        "off-label": "OFF",
        "on-show-text": true,
        "on-show-icon": true,
        "on-icon-src": null,
        "on-icon-class": null,
        "on-tooltip": null,
        "off-show-text": true,
        "off-show-icon": true,
        "off-icon-src": null,
        "off-icon-class": null,
        "off-tooltip": null,
        "confirm-on-text": "${V_CONFIRM_ON}",
        "confirm-off-text": "${V_CONFIRM_OFF}",
        "show-icon": true,
        "show-text": true,
        "on-action": [],
        "off-action": [],
        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined" || !self.config['show-text']) {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            labelEl: null,
            buttonEl: null,
            offStateEl: null,
            onStateEl: null,
            onLabelEl: null,
            offLabelEl: null,
            iconEl: null,
            onIconEl: null,
            offIconEl: null,

            initialized: false,
            subscriptionTargetId: null,
            onAction: null,
            offAction: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    core = shmi.requires("visuals.core"),
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.buttonEl = shmi.getUiElement('button', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                self.vars.offStateEl = shmi.getUiElement('off-state', self.element);
                if (self.vars.offStateEl) {
                    self.vars.offLabelEl = shmi.getUiElement('label', self.vars.offStateEl);
                    self.vars.offIconEl = shmi.getUiElement('toggle-icon', self.vars.offStateEl);

                    if (!self.vars.offLabelEl || !self.config['off-label'] || !self.config['off-show-text']) {
                        shmi.addClass(self.vars.offStateEl, "no-label");
                    } else {
                        self.vars.offLabelEl.textContent = shmi.localize(self.config['off-label']);
                    }

                    if (!self.vars.offIconEl || !self.config['off-show-icon'] || !(self.config['off-icon-class'] || self.config['off-icon-src'])) {
                        shmi.addClass(self.vars.offStateEl, "no-icon");
                    } else {
                        self.setupIconElement(self.vars.offIconEl, self.config['off-icon-class'], self.config['off-icon-src'], self.config['off-show-icon'], self.config['off-show-text']);
                    }
                }

                self.vars.onStateEl = shmi.getUiElement('on-state', self.element);
                if (self.vars.onStateEl) {
                    self.vars.onLabelEl = shmi.getUiElement('label', self.vars.onStateEl);
                    self.vars.onIconEl = shmi.getUiElement('toggle-icon', self.vars.onStateEl);

                    if (!self.vars.onLabelEl || !self.config['on-label'] || !self.config['on-show-text']) {
                        shmi.addClass(self.vars.onStateEl, "no-label");
                    } else {
                        self.vars.onLabelEl.textContent = shmi.localize(self.config['on-label']);
                    }

                    if (!self.vars.onIconEl || !self.config['on-show-icon'] || !(self.config['on-icon-class'] || self.config['on-icon-src'])) {
                        shmi.addClass(self.vars.onStateEl, "no-icon");
                    } else {
                        self.setupIconElement(self.vars.onIconEl, self.config['on-icon-class'], self.config['on-icon-src'], self.config['on-show-icon'], self.config['on-show-text']);
                    }
                }

                if (!self.vars.offStateEl || !self.vars.onStateEl) {
                    shmi.log("[IQ:iq-button-toggle] At least one state element is missing", 1);
                    return;
                }

                // Label
                setLabelImpl(self, self.config.label);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.addClass(self.element, "no-icon");
                    shmi.log('[IQ:iq-button-rocker] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                const eventFunctions = {
                    onPress: function() {
                        shmi.log("[IQ:iq-button-toggle] Button pressed", 0);
                        shmi.addClass(self.element, 'pressed');
                    },
                    onRelease: function() {
                        shmi.log("[IQ:iq-button-toggle] Button released", 0);
                        shmi.removeClass(self.element, 'pressed');
                    },
                    onClick: function() {
                        if ((self.vars.value === self.config['on-value'])) {
                            // TURN OFF
                            if (self.config['confirm-off']) {
                                shmi.confirm(self.config['confirm-off-text'], function(conf) {
                                    if (conf) {
                                        self.toggle();
                                    }
                                });
                            } else {
                                self.toggle();
                            }
                            // TURN ON
                        } else if (self.config['confirm-on']) {
                            shmi.confirm(self.config['confirm-on-text'], function(conf) {
                                if (conf) {
                                    self.toggle();
                                }
                            });
                        } else {
                            self.toggle();
                        }
                    }
                };

                if (!self.vars.buttonEl) {
                    shmi.log('[IQ:iq-button-rocker] no button element provided', 1);
                } else {
                    self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.buttonEl, eventFunctions));
                    self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.buttonEl, eventFunctions));
                }

                // Reset on- and off-values in case they are set and only an event is set
                if (!self.config.item && self.config.event) {
                    self.config['on-value'] = 1;
                    self.config['off-value'] = 0;
                }

                // Initialize with OFF value
                self.vars.value = self.config['off-value'];

                /******************/
                /*** UI Actions ***/
                /******************/
                if (Array.isArray(self.config["on-action"]) && self.config["on-action"].length) {
                    self.vars.onAction = new core.UiAction(self.config["on-action"], self);
                }
                if (Array.isArray(self.config["off-action"]) && self.config["off-action"].length) {
                    self.vars.offAction = new core.UiAction(self.config["off-action"], self);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-button-toggle] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-button-toggle] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-button-toggle] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-button-toggle] unlocked", 1);
            },

            /**
             * Toggles the state of the Toggle Button
             */
            toggle: function() {
                const self = this,
                    turnOn = (self.vars.value !== self.config["on-value"]);

                // Item / value
                if (self.config.item) {
                    if (turnOn) {
                        self.imports.im.writeValue(self.config.item, self.config['on-value']);
                    } else {
                        self.imports.im.writeValue(self.config.item, self.config['off-value']);
                    }
                } else if (!turnOn) {
                    self.setValue(self.config['off-value']);
                } else {
                    self.setValue(self.config['on-value']);
                }

                // UI Actions
                if (turnOn && self.vars.onAction) {
                    self.vars.onAction.execute();
                } else if (!turnOn && self.vars.offAction) {
                    self.vars.offAction.execute();
                }
            },

            /**
             * Gets the best tooltip value for the buttons current state.
             *
             * @override
             */
            getTooltip: function() {
                const self = this,
                    superTooltip = shmi.visuals.core.BaseControl.prototype.getTooltip.call(this);

                if (superTooltip) {
                    return superTooltip;
                } else if (self.vars.value === self.config['off-value']) {
                    if (shmi.objectHasOwnProperty(self.config, "off-tooltip")) {
                        return self.config['off-tooltip'];
                    }
                } else if (self.vars.value === self.config['on-value']) {
                    if (shmi.objectHasOwnProperty(self.config, "on-tooltip")) {
                        return self.config['on-tooltip'];
                    }
                }

                return null;
            },

            /**
             * Sets the current value of the Toggle Button
             *
             * @param {string|int|float} value - new value to set
             * @param {string} type
             * @param {string} name
             */
            onSetValue: function(value, type, name) {
                const self = this,
                    oldValue = self.vars.value;

                self.vars.value = parseFloat(value);

                // Show/hide elements
                if (self.vars.value === self.config['off-value']) {
                    shmi.removeClass(self.vars.buttonEl, "iq-on-state");
                    shmi.addClass(self.vars.buttonEl, "iq-off-state");
                } else {
                    shmi.removeClass(self.vars.buttonEl, "iq-off-state");
                    shmi.addClass(self.vars.buttonEl, "iq-on-state");
                }

                // Has changed -> fire events
                if (self.vars.value !== oldValue) {
                    self.setTooltip(self.getTooltip());
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves the current value ot the Toggle Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Sets up the icon depending on the configuration
             *
             * @param {object} iconEl
             * @param {string} iconClass
             * @param {string} iconSrc
             */
            setupIconElement: function(iconEl, iconClass, iconSrc) {
                const { applyIcon } = shmi.requires("visuals.tools.icons");

                if (iconClass) {
                    applyIcon(iconEl, null, iconClass);
                } else if (iconSrc) {
                    applyIcon(iconEl, iconSrc, null, this.config["inline-svg-icon"]);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-button
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-button",
 *     "name": null,
 *     "template": "default/iq-button"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "numpad-enabled": Whether the numpad is enabled
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 * "action-release" UI Action on button release
 * "action-while-pressed": UI Action while pressed
 * "interval-while-pressed": UI Action interval while pressed
 * "disable-alarms": Whether to prevent the default alarm styling when alarm/prewarn limits are reached. By disabling you can manually set the classes warn/preWarn via a condition on the widget.
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-button", // control name in camel-case
        uiType = "iq-button", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-button iq-variant-01",
        "name": null,
        "template": "default/iq-button.iq-variant-01",

        "item": null,

        "label": '[Label]',
        "auto-label": true,
        "label-from-item": false,

        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": true,

        "action": null,
        "onClick": null,

        "tooltip": null,

        "write-bool": false,
        "on-value": 1,
        "off-value": 0,

        "action-release": null,
        "action-while-pressed": null,
        "interval-while-pressed": null,
        "action-pressed": null,

        "disable-alarms": false,

        "show-icon": true
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Resets the button to a neutral/released state, disables all on press actions
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setNeutralState(self) {
        shmi.removeClass(self.vars.buttonEl, 'pressed');
        if (self.config['write-bool'] && self.config.item && !self.config.monoflop) {
            self.writeValue(self.config["off-value"]);
        }

        // Stop any running timer on release
        if (self.vars.actionWhilePressedTimer) {
            clearInterval(self.vars.actionWhilePressedTimer);
            self.vars.actionWhilePressedTimer = null;
        }

        if (self.vars.actionRelease) {
            self.vars.actionRelease.execute();
        }
        self.vars.currentlyPressed = false;
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        tooltipProperties: ['tooltip'],

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            buttonEl: null,
            labelEl: null,
            iconEl: null,

            label: null,
            action: null,
            actionPress: null,
            actionRelease: null,
            actionWhilePressed: null,
            actionWhilePressedTimer: null,
            currentlyPressed: false,

            // For handling keyboard input on button (ENTER)
            keyDownListener: null,
            keyUpListener: null,
            keyDownOnMe: null,

            mouseListener: null,
            touchListener: null,
            initialized: false,
            rafId: 0,
            subscriptionTargetId: null,

            monoFlopInterval: 0,

            // Conditional Control class
            conditional: null,

            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ['press', 'release', 'click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /***************/
            /*** ON INIT ***/
            /***************/
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                self.imports.nv.initValueSettings(self);

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.buttonEl = shmi.getUiElement('button', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-button] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                    shmi.addClass(self.vars.iconEl, "inlined");
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Label and icon are optional, but button element is required
                if (!self.vars.buttonEl) {
                    shmi.log("[IQ:iq-button] template is missing button element", 3);
                    self.vars.buttonEl = null;
                    self.vars.labelEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                // Label
                setLabelImpl(self, self.config.label);

                // UI Actions
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }
                if (self.config['action-press']) {
                    self.vars.actionPress = new shmi.visuals.core.UiAction(self.config['action-press'], self);
                }
                if (self.config['action-release']) {
                    self.vars.actionRelease = new shmi.visuals.core.UiAction(self.config['action-release'], self);
                }
                if (self.config['action-while-pressed']) {
                    self.vars.actionWhilePressed = new shmi.visuals.core.UiAction(self.config['action-while-pressed'], self);
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                // PREV BUTTON
                var session = shmi.visuals.session;
                var btnFuncs = {
                    onClick: function(x, y, e) {
                        if (session.FocusElement !== null) {
                            session.FocusElement.blur();
                            session.FocusElement = null;
                        }
                        if (self.vars.buttonEl instanceof HTMLElement) {
                            self.vars.buttonEl.focus();
                            session.FocusElement = self.vars.buttonEl;
                            shmi.log("[IQ:iq-button] focused", 1);
                        } else {
                            shmi.log("[IQ:iq-button] only HTMLElements may be focused, type: " + (self.vars.buttonEl.constructor), 1);
                        }

                        self.fire('click', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }

                        if (self.onClick) {
                            self.onClick(self);
                        }
                    },
                    onRelease: function(x, y, e) {
                        self.fire('release', {
                            x: x,
                            y: y,
                            event: e
                        });
                        setNeutralState(self);
                    },
                    onPress: function(x, y, e) {
                        self.vars.currentlyPressed = true;
                        shmi.addClass(self.vars.buttonEl, 'pressed');
                        self.fire('press', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.config['write-bool'] && self.config.item && !self.config.monoflop) {
                            self.writeValue(self.config["on-value"]);
                        }

                        // UI Action: onPress
                        if (self.vars.actionPress) {
                            self.vars.actionPress.execute();
                        }

                        // UI Action while pressed
                        if (self.vars.actionWhilePressed && self.config['interval-while-pressed']) {
                            if (self.vars.actionWhilePressedTimer) {
                                clearInterval(self.vars.actionWhilePressedTimer);
                            }

                            // While pressed timer
                            self.vars.actionWhilePressedTimer = setInterval(function() {
                                self.vars.actionWhilePressed.execute();
                            }, self.config['interval-while-pressed']);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.buttonEl, btnFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.buttonEl, btnFuncs));

                // Key-up listener
                self.vars.keyUpListener = function(e) {
                    const valid = self.vars.keyDownOnMe;
                    if (valid && (e.code === 'Enter' || e.code === 'NumpadEnter') && self.vars.buttonEl === document.activeElement) {
                        shmi.removeClass(self.vars.buttonEl, 'pressed');
                        const rect = self.vars.buttonEl.getBoundingClientRect();
                        btnFuncs.onClick(rect.left + (rect.width / 2), rect.top + (rect.height / 2), e);
                        self.vars.keyDownOnMe = false;
                    }
                };

                // Key-down listener
                self.vars.keyDownListener = function(e) {
                    if ((e.code === 'Enter' || e.code === 'NumpadEnter') && self.vars.buttonEl === document.activeElement) {
                        shmi.addClass(self.vars.buttonEl, 'pressed');
                        self.vars.keyDownOnMe = true;
                    }
                };
            },

            /*****************/
            /*** ON ENABLE ***/
            /*****************/
            onEnable: function() {
                var self = this;

                // Conditional
                if (self.vars.conditional !== null) {
                    if (self.vars.conditional.item !== self.config.item) {
                        self.vars.conditional.item = self.config.item;
                    }
                } else if (self.config.item && !self.config['disable-alarms']) {
                    self.vars.conditional = shmi.createConditional(self.element, self.config.item);
                }

                if (self.vars.conditional) {
                    self.vars.conditional.enable();
                }

                self.vars.buttonEl?.setAttribute('tabindex', '0');

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                // Has Item
                if (self.config.item) {
                    // Item Lock
                    if (self.config["disable-item-lock"]) {
                        var h = self.imports.im.getItemHandler();
                        h.setValue = function(value) {
                            self.setValue(value);
                        };
                        self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, h);
                    } else {
                        self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                    }

                    // Monoflop
                    if (self.config.monoflop) {
                        var pTok = self.listen("press", function() {
                            clearInterval(self.vars.monoFlopInterval);
                            self.vars.monoFlopInterval = setInterval(function() {
                                self.imports.im.writeValue(self.config.item, self.config['monoflop-value'], { skipSameValueCheck: true });
                            }, self.config['monoflop-interval']);
                        });
                        var rTok = self.listen("release", function() {
                            clearInterval(self.vars.monoFlopInterval);
                        });
                        self.vars.tokens.push(pTok, rTok);
                    }
                }

                // Key listeners
                self.element.addEventListener('keyup', self.vars.keyUpListener, false);
                self.element.addEventListener('keydown', self.vars.keyDownListener, false);

                shmi.log("[IQ:iq-button] Enabled", 1);
            },

            /******************/
            /*** ON DISABLE ***/
            /******************/
            onDisable: function() {
                var self = this;

                // Conditional
                if (self.vars.conditional) {
                    self.vars.conditional.disable();
                }

                // Unsubscribe Item
                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                // Restore tabindex
                self.vars.buttonEl?.removeAttribute('tabindex');

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                // Disable listeners
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.element.removeEventListener('keyup', self.vars.keyUpListener);
                self.element.removeEventListener('keydown', self.vars.keyDownListener);

                // Disable monoflop
                if (self.vars.monoFlopInterval) {
                    clearInterval(self.vars.monoFlopInterval);
                    self.vars.monoFlopInterval = 0;
                }

                // reset button if pressed currently
                if (self.vars.currentlyPressed) {
                    setNeutralState(self);
                }

                shmi.log("[IQ:iq-button] disabled", 1);
            },

            /***************/
            /*** ON LOCK ***/
            /***************/
            onLock: function() {
                var self = this;

                // Disable listeners
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                // reset button if pressed currently
                if (self.vars.currentlyPressed) {
                    setNeutralState(self);
                }

                // Disable monoflop
                if (self.vars.monoFlopInterval) {
                    clearInterval(self.vars.monoFlopInterval);
                    self.vars.monoFlopInterval = 0;
                }

                if (self.vars.buttonEl) {
                    self.vars.buttonEl.removeAttribute('tabindex');
                    self.vars.buttonEl.removeEventListener('keyup', self.vars.keyUpListener);
                    self.vars.buttonEl.removeEventListener('keydown', self.vars.keyDownListener);
                    self.vars.buttonEl.blur();
                }

                // CSS
                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-button] Locked", 1);
            },

            /*****************/
            /*** ON UNLOCK ***/
            /*****************/
            onUnlock: function() {
                var self = this;

                // Enable listeners
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                // CSS
                shmi.removeClass(self.element, 'locked');

                if (self.vars.buttonEl) {
                    self.vars.buttonEl.addEventListener('keyup', self.vars.keyUpListener, false);
                    self.vars.buttonEl.addEventListener('keydown', self.vars.keyDownListener, false);
                    self.vars.buttonEl.setAttribute('tabindex', 0);
                }

                shmi.log("[IQ:iq-button] unlocked", 1);
            },

            /*******************/
            /** ON SET VALUE ***/
            /*******************/
            onSetValue: function(value, type, name) {
                var self = this;
                self.vars.value = value;
                if (self.config['label-from-item'] === true) {
                    shmi.caf(self.rafId);
                    self.rafId = shmi.raf(function() {
                        setLabelImpl(self, shmi.localize(self.imports.nv.formatOutput(value, self)));
                    });
                }
            },

            /*****************/
            /*** GET VALUE ***/
            /*****************/
            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;
                return self.vars.value;
            },

            /*************************/
            /*** ON SET PROPERTIES ***/
            /*************************/
            onSetProperties: function(min, max, step) {
                var self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            /********************/
            /*** ON SET LABEL ***/
            /********************/
            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Writes 1/0 to connected data source
             */
            writeValue: function(value) {
                var self = this;
                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, value, { skipSameValueCheck: true });
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-checkbox
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-checkbox",
 *     "name": null,
 *     "template": "default/iq-checkbox"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "type": Type is INT
 * "auto-label": Whether to use the auto-label (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-checkbox", // control name in camel-case
        uiType = "iq-checkbox", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-checkbox iq-variant-01",
        "name": null,
        "template": "default/iq-checkbox.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "type": shmi.c("TYPE_INT"),
        "auto-label": true,
        "show-icon": true,
        "show-text": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "on-value": 1,
        "off-value": 0,
        "pressed-class": "pressed",
        "confirm-off-text": "${V_CONFIRM_OFF}",
        "confirm-on-text": "${V_CONFIRM_ON}",
        "confirm-on": false,
        "confirm-off": false,
        "inline-svg": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            checkboxEl: null,
            labelEl: null,
            iconEl: null,
            containerEl: null,

            label: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ['change'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.checkboxEl = shmi.getUiElement('checkbox', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.containerEl = shmi.getUiElement('iq-checkbox-container', self.element);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-checkbox] no button-icon element provided', 1);
                    shmi.addClass(self.element, "no-icon");
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                self.vars.value = self.config["off-value"];

                // Label is optional, but prev, next and current are req'd for all variants
                if (self.vars.checkboxEl === null) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-checkbox] Checkbox element is missing", 3);
                    self.vars.checkboxEl = null;
                    self.vars.labelEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                // Label
                if (self.config["show-text"]) {
                    setLabelImpl(self, self.config.label);
                } else {
                    setLabelImpl(self, null);
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                if (self.vars.containerEl) {
                    const fieldFuncs = {
                        onPress: function(x, y, event) {
                            shmi.addClass(self.element, self.config["pressed-class"]);
                        },
                        onRelease: function() {
                            shmi.removeClass(self.element, self.config["pressed-class"]);
                        },
                        onClick: function(x, y, e) {
                            e.preventDefault();
                            if (self.vars.value === self.config['on-value']) {
                                // IS ON -> OFF
                                if (self.config['confirm-off']) {
                                    shmi.confirm(self.config['confirm-off-text'], function onConfirmed(confirmed) {
                                        if (confirmed) {
                                            self.updateValue(self.config['off-value']);
                                        }
                                    });
                                } else {
                                    self.updateValue(self.config['off-value']);
                                }
                            } else if (self.config['confirm-on']) {
                                // IS OFF -> ON
                                shmi.confirm(self.config['confirm-on-text'], function onConfirmed(confirmed) {
                                    if (confirmed) {
                                        self.updateValue(self.config['on-value']);
                                    }
                                });
                            } else {
                                self.updateValue(self.config['on-value']);
                            }
                        }
                    };
                    self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.containerEl, fieldFuncs));
                    self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.containerEl, fieldFuncs));
                } else {
                    console.error("[IQ:iq-checkbox] 'iq-checkbox-container' element is missing, cannot attach listener.");
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-checkbox] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-checkbox] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                self.vars.checkboxEl.blur();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(this.element, 'locked');
                self.vars.checkboxEl.disabled = true;

                shmi.log("[IQ:iq-checkbox] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');
                self.vars.checkboxEl.disabled = false;

                shmi.log("[IQ:iq-checkbox] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this;

                let changed = false;

                if (value === self.config['on-value']) {
                    self.vars.value = self.config['on-value'];
                    changed = true;
                    shmi.addClass(self.element, 'checked');
                } else {
                    self.vars.value = self.config['off-value'];
                    changed = true;
                    shmi.removeClass(self.element, 'checked');
                }
                if (changed) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Writes current value to connected data source
             */
            updateValue: function(value) {
                const self = this;
                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, value);
                } else {
                    self.setValue(value);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-date-time
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-date-time",
 *     "name": null,
 *     "template": "custom/controls/iq-date-time"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * display-utc {boolean}: Whether or not the displayed time should be UTC or not
 * display-format {string}: Format string for the displayed date
 * input-format {string}: Format string to use when parsing the items value
 * invalid-text {string}: String to display if the item does not contain a valid timestamp.
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iqDateTime", // control name in camel-case
        uiType = "iq-date-time", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-date-time iq-variant-01",
        "name": null,
        "template": "default/iq-date-time.iq-variant-01",
        "item": null,
        "display-utc": false,
        "display-format": "$YYYY-$MM-$DD $HH:$mm:$ss",
        "input-format": "$X",
        "invalid-text": "${audit-log-viewer.eventOngoing}",
        "tooltip": null,
        "value-as-tooltip": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            domEl: null,

            subscriptionTargetId: null,
            localizedDisplayFormat: null,
            localizedInvalidText: null,
            rafRunning: false,
            lastValue: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dt: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.domEl = shmi.getUiElement('datetime', self.element);
                if (!self.vars.domEl) {
                    shmi.log("[IQ:iq-date-time] Element is missing", 3);
                    return;
                }

                self.vars.localizedDisplayFormat = shmi.localize(self.config["display-format"]);
                self.vars.localizedInvalidText = shmi.localize(self.config["invalid-text"]);

                self.vars.domEl.textContent = self.vars.localizedInvalidText;
                if (self.config["value-as-tooltip"]) {
                    self.setTooltip(self.vars.localizedInvalidText);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-date-time] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.subscriptionTargetId) {
                    self.vars.subscriptionTargetId.unlisten();
                    self.vars.subscriptionTargetId = null;
                }

                shmi.log("[IQ:iq-date-time] disabled", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type) {
                var self = this;

                if (type === shmi.c("TYPE_INT") || type === shmi.c("TYPE_FLOAT")) {
                    self.vars.lastValue = self.imports.dt.parseDateTime(String(value), self.config["input-format"] || "$X");
                } else if (!self.config["input-format"]) {
                    self.vars.lastValue = null;
                } else {
                    self.vars.lastValue = self.imports.dt.parseDateTime(String(value), self.config["input-format"]);
                }

                // Only attempt to update the DOM during an animation frame.
                // Prevents floods of redraws and may perform better anyway
                if (!self.vars.rafRunning) {
                    self.vars.rafRunning = true;
                    shmi.raf(function onDraw() {
                        let localizedText = null;
                        if (!self.vars.lastValue) {
                            localizedText = self.vars.localizedInvalidText;
                        } else {
                            localizedText = self.imports.dt.formatDateTime(self.vars.lastValue, {
                                datestring: self.vars.localizedDisplayFormat,
                                utc: self.config["display-utc"]
                            });
                        }

                        self.vars.domEl.textContent = localizedText;
                        self.vars.domEl.innerHTML = self.vars.domEl.innerHTML.replace(/\n/g, "<br>");
                        if (self.config["value-as-tooltip"]) {
                            self.setTooltip(localizedText.replace(/(\\n)+/g, " "));
                        }

                        self.vars.rafRunning = false;
                    });
                }
            },
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-duration-display
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-duration-display",
 *     "name": null,
 *     "template": "custom/controls/iq-duration-display"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "target-ts-item": The target time item
 * "target-ts-format": The date format of the target time item
 * "current-ts-item": The current time item
 * "current-ts-format": The date format of the current time item
 * "display-preset": Different display formats
 * "invalid-text": Text shown when an invalid time occurs
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-duration-display", // control name in camel-case
        uiType = "iq-duration-display", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-duration-display iq-variant-01",
        "name": null,
        "template": "default/iq-duration-display.iq-variant-01",
        "label": '[Label]',
        "target-ts-item": null,
        "target-ts-format": "$X",
        "current-ts-item": "Systemzeit",
        "current-ts-format": "$X",
        "display-preset": "compact",
        "invalid-text": "${duration-display.invalid-duration}",
        "tooltip": null,
        "value-as-tooltip": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the displayed text and tooltip of the control.
     *
     * @param {*} self Reference to the control.
     * @param {string} str Text to display.
     */
    function setText(self, str) {
        if (self.vars.durationEl) {
            self.vars.durationEl.textContent = str;
            if (self.config["value-as-tooltip"]) {
                self.setTooltip(str);
            }
        }
    }

    function doUpdate(self) {
        if (self.vars.tsTarget === null || self.vars.tsCurrent === null) {
            self.vars.lastValue = null;
        } else {
            self.vars.lastValue = self.vars.tsTarget - self.vars.tsCurrent;
        }

        if (!self.vars.rafRunning) {
            self.vars.rafRunning = true;
            shmi.raf(function onDraw() {
                if (!self.vars.lastValue) {
                    setText(self, self.vars.localizedInvalidText);
                } else {
                    setText(self, self.imports.dt.formatDuration(self.vars.lastValue, self.config["display-preset"] || "compact"));
                }

                self.vars.rafRunning = false;
            });
        }
    }

    function makeItemHandler(self, configFormatName, destVarName) {
        return {
            setValue: function setValue(value, type) {
                if (type === shmi.c("TYPE_INT") || type === shmi.c("TYPE_FLOAT")) {
                    self.vars[destVarName] = self.imports.dt.parseDateTime(String(value), self.config[configFormatName] || "$X");
                } else if (!self.config[configFormatName]) {
                    self.vars[destVarName] = null;
                } else {
                    self.vars[destVarName] = self.imports.dt.parseDateTime(String(value), self.config[configFormatName]);
                }

                doUpdate(self);
            }
        };
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            tsTarget: null,
            tsCurrent: null,
            localizedInvalidText: null,
            rafRunning: false,
            lastValue: null,

            // DOM elements
            durationEl: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dt: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                self.vars.durationEl = shmi.getUiElement('duration', self.element);
                if (!self.vars.durationEl) {
                    shmi.log("[IQ:iq-duration] Element is missing", 3);
                    return;
                }

                self.vars.localizedInvalidText = shmi.localize(self.config["invalid-text"]);
                setText(self, self.vars.localizedInvalidText);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;
                if (self.config["target-ts-item"] && self.config["current-ts-item"] && self.vars.listeners.length === 0) {
                    self.vars.listeners.push(self.imports.im.subscribeItem(self.config["target-ts-item"], makeItemHandler(self, "target-ts-format", "tsTarget")));
                    self.vars.listeners.push(self.imports.im.subscribeItem(self.config["current-ts-item"], makeItemHandler(self, "current-ts-format", "tsCurrent")));
                }
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.vars.listeners.forEach(function(sub) {
                    sub.unlisten();
                });
                self.vars.listeners = [];
            },
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-dynamic-svg-image
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-dynamic-svg-image",
 *     "name": null,
 *     "template": "default/iq-dynamic-svg-image"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "image-src" {string}: The image source
 * "image-alt" {string}: The alt text
 * "scaling-mode" {string}: The scaling mode for the SVG
 * selectors {object}: configured selectors with their item value / styling / ui-action
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iqDynamicSvgImage", // control name in camel-case
        uiType = "iq-dynamic-svg-image", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-dynamic-svg-image",
        "name": null,
        "template": "default/iq-dynamic-svg-image.iq-variant-01",
        "image-src": "pics/system/icons/placeholder.svg",
        "image-alt": null,
        "scaling-mode": "none",
        "selectors": null
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * applyScalingMode - applys the scaling mode css class according to config
     *
     * @param {object} self widget instance
     */
    function applyScalingMode(self) {
        let scalingMode = null;

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        if (scalingMode) shmi.addClass(self.element, scalingMode);
    }
    /**
     * getMatch - searches and returns css selector / id matches
     *
     * @param {object} self widget instance
     * @param {object} selector selector configuration
     * @return {object} matches matches HTMLCollection
     */
    function getMatch(self, selector) {
        let currentSelector = selector.selector,
            matches = [];

        if (!currentSelector) {
            return matches;
        }

        if (selector.selector.startsWith(".")) {
            currentSelector = selector.selector.substr(1);
        }

        const isID = selector.selector.startsWith("#");
        if (isID) {
            try {
                matches = Array.from(self.vars.imageEl.querySelectorAll(currentSelector)).slice(0, 1);
            } catch (e) {
                console.error("[iq-dynamic-svg-image] Malformed selector: " + currentSelector);
                shmi.notify("[iq-dynamic-svg-image] Malformed selector: " + currentSelector);
            }
        } else {
            try {
                matches = Array.from(self.vars.imageEl.getElementsByClassName(currentSelector));
            } catch (e) {
                console.error("[iq-dynamic-svg-image] Malformed selector: " + currentSelector);
                shmi.notify("[iq-dynamic-svg-image] Malformed selector: " + currentSelector);
            }
        }
        return matches;
    }

    /**
     * removeMatchBlink - removes all blink related classes from a match
     *
     * @param {object} matches matches object
     * @param {object} subval subval configuration
     */
    function removeMatchBlink(match) {
        match.classList.remove("svg-blink-visible", "svg-blink-hidden", "svg-blink-slow", "svg-blink-medium", "svg-blink-fast");
    }

    /**
     * applyMatchStyle - applys the custom styling to the final match
     *
     * @param {object} matches matches object
     * @param {object} subval subval configuration
     */
    function applyMatchStyle(match, subval) {
        if (subval.fillColor.value?.variable) {
            match.style.fill = `var(${subval.fillColor.value.variable})`;
            if (subval.fillColor.value.transparency) {
                match.style["fill-opacity"] = 1 - subval.fillColor.value.transparency / 100;
            } else {
                match.style["fill-opacity"] = "initial";
            }
        } else {
            match.style.fill = subval.fillColor.value;
            match.style["fill-opacity"] = "initial";
        }
        if (subval.strokeColor.value?.variable) {
            match.style.stroke = `var(${subval.strokeColor.value.variable})`;
            if (subval.strokeColor.value.transparency) {
                match.style["stroke-opacity"] = 1 - subval.strokeColor.value.transparency / 100;
            } else {
                match.style["stroke-opacity"] = "initial";
            }
        } else {
            match.style.stroke = subval.strokeColor.value;
            match.style["stroke-opacity"] = "initial";
        }
        match.style["stroke-width"] = subval.strokeWidth.value;
        removeMatchBlink(match);
        if (subval.blink.value) {
            match.classList.add("svg-blink-" + subval.blink.value);
        }
    }

    /**
     * getRandomId - generates random ids for our stylecache
     *
     * @return {string} random id
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function applyStyleCache(self, match) {
        const toApply = {
            fillColor: { index: -1, valIndex: -1, value: null },
            strokeColor: { index: -1, valIndex: -1, value: null },
            strokeWidth: { index: -1, valIndex: -1, value: null },
            blink: { index: -1, valIndex: -1, value: null }
        };

        self.vars.appliedStyleCache[match.id].forEach((style) => {
            // reset toapply val mapping index if selector index is higher
            if (style.index > toApply.fillColor.index && style.fillColor) {
                toApply.fillColor.valIndex = style.valIndex;
            }
            if (style.index > toApply.strokeColor.index && style.strokeColor) {
                toApply.strokeColor.valIndex = style.valIndex;
            }
            if (style.index > toApply.strokeWidth.index && style.strokeWidth) {
                toApply.strokeWidth.valIndex = style.valIndex;
            }
            if (style.index > toApply.blink.index && style.blink && style.blink !== "none") {
                toApply.blink.valIndex = style.valIndex;
            }
            // set toapply style if it's of higher priority than the current style
            if (style.index >= toApply.fillColor.index && style.valIndex >= toApply.fillColor.valIndex && style.fillColor) {
                toApply.fillColor.value = style.fillColor;
                toApply.fillColor.index = style.index;
                toApply.fillColor.valIndex = style.valIndex;
            }
            if (style.index >= toApply.strokeColor.index && style.valIndex >= toApply.strokeColor.valIndex && style.strokeColor) {
                toApply.strokeColor.value = style.strokeColor;
                toApply.strokeColor.index = style.index;
                toApply.strokeColor.valIndex = style.valIndex;
            }
            if (style.index >= toApply.strokeWidth.index && style.valIndex >= toApply.strokeWidth.valIndex && style.strokeWidth) {
                toApply.strokeWidth.value = style.strokeWidth;
                toApply.strokeWidth.index = style.index;
                toApply.strokeWidth.valIndex = style.valIndex;
            }
            if (style.index >= toApply.blink.index && style.valIndex >= toApply.blink.valIndex && style.blink && style.blink !== "none") {
                toApply.blink.value = style.blink;
                toApply.blink.index = style.index;
                toApply.blink.valIndex = style.valIndex;
            }
        });

        applyMatchStyle(match, toApply);
    }

    /**
     * applyStyle - applys the custom styling according to the subval config
     *
     * @param {object} self widget instance
     * @param {object} selector selector configuration
     * @param {object} matches matches object
     * @param {string} name item name
     * @param {object} subval subval configuration
     * @param {number} selectorIdx selector index
     */
    function applyStyle(self, selector, matches, name, subval, selectorIdx, valIdx) {
        for (const match of matches) {
            self.vars.appliedStyleCache[match.id].push({
                index: selectorIdx,
                valIndex: valIdx,
                itemName: name,
                fillColor: subval.fillColor,
                strokeColor: subval.strokeColor,
                strokeWidth: subval.strokeWidth,
                blink: subval.blink
            });
            if (match.children && match.tagName === "g") { // svg group
                applyStyle(self, selector, match.children, name, subval, selectorIdx, valIdx);
            }

            applyStyleCache(self, match);
        }
    }

    /**
     * removeStyle - removes the custom styling according to the subval config from all sub elements
     *
     * @param {object} self widget instance
     * @param {object} selector selector configuration
     * @param {object} matches matches object
     * @param {string} name item name
     * @param {number} selectorIdx selector index
     */
    function removeStyle(self, selector, matches, name, selectorIdx, valIdx) {
        for (const match of matches) {
            if (self.vars.appliedStyleCache[match.id] && self.vars.appliedStyleCache[match.id].length) {
                const newCache = shmi.cloneObject(self.vars.appliedStyleCache[match.id]);
                self.vars.appliedStyleCache[match.id].forEach(function(itemStyle, idx) {
                    if (itemStyle.itemName === name && itemStyle.index === selectorIdx && itemStyle.valIndex === valIdx) {
                        newCache.splice(idx, 1);
                    }
                });
                self.vars.appliedStyleCache[match.id] = newCache;
            }

            if (match.children && match.tagName === "g") { // svg group
                removeStyle(self, selector, match.children, name, selectorIdx, valIdx);
            }
            if (self.vars.appliedStyleCache[match.id] && self.vars.appliedStyleCache[match.id].length) {
                applyStyleCache(self, match);
            } else {
                match.style.fill = null;
                match.style.stroke = null;
                match.style["stroke-width"] = null;
                removeMatchBlink(match);
            }
        }
    }

    /**
     * rebuildViewBoxIfNeeded - adds a viewbox to svgs missing it but having proper width and height descriptors
     *
     * @param {object} self widget instance
     */
    function rebuildViewBoxIfNeeded(self) {
        if (self.vars.imageEl && self.vars.imageEl.firstElementChild && !self.vars.imageEl.firstElementChild.getAttribute("viewBox")) {
            const width = self.vars.imageEl.firstElementChild.getAttribute("width");
            const height = self.vars.imageEl.firstElementChild.getAttribute("height");

            if (width && height) {
                self.vars.imageEl.firstElementChild.setAttribute("viewBox", `0 0 ${width} ${height}`);
            }
        }
    }

    /**
     * initStyleCache - initializes the style cache for the match element
     *
     * @param {object} self widget instance
     * @param {object} match match element
     */
    function initStyleCache(self, match) {
        if (!match.id) {
            do {
                match.id = getRandomId();
            } while (self.vars.appliedStyleCache[match.id]);
        }
        self.vars.appliedStyleCache[match.id] = self.vars.appliedStyleCache[match.id] || [];
    }

    /**
     * initSubElements - recursively initialize all elements with their sub elements
     *
     * @param {object} self widget instance
     * @param {object} matches matches object
     * @param {object} selector selector data object
     */
    function initSubElements(self, matches, selector) {
        const clickHandler = {
            onClick: function(x, y, e) {
                self.fire("click", {
                    x: x,
                    y: y,
                    event: e
                });

                const targetSelector = e.srcElement.svgClickTargetSelector;

                if (self.vars.actions[targetSelector]) {
                    self.vars.actions[targetSelector].forEach((action) => action.execute());
                }
            }
        };
        self.vars.actions = self.vars.actions || {};
        for (const match of matches) {
            if (match.children && match.tagName === "g") { // svg group
                initSubElements(self, match.children, selector);
            }
            initStyleCache(self, match);
            if (self.vars.appliedStyleCache[match.id].length === 0) {
                self.vars.appliedStyleCache[match.id].push({
                    index: -1,
                    valIndex: -1,
                    fillColor: match.style.fill,
                    strokeColor: match.style.stroke,
                    strokeWidth: match.style["stroke-width"],
                    blink: null
                });
            }
            if (Array.isArray(selector.selectorAction) && selector.selectorAction.length > 0) {
                self.vars.actions[selector.selector] = self.vars.actions[selector.selector] ?? [];
                self.vars.actions[selector.selector].push(new shmi.visuals.core.UiAction(selector.selectorAction, self));
                shmi.addClass(match, 'clickable');
                match.svgClickTargetSelector = selector.selector;
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(match, clickHandler));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(match, clickHandler));
            }
        }
    }

    /**
     * initializeImageListenersAndCache - intializes all ui action listeners and builds the initial style cache recursively
     *
     * @param {object} self widget instance
     */
    function initializeImageListenersAndCache(self) {
        if (Array.isArray(self.config.selectors)) {
            self.config.selectors.forEach(function(selector, idx) {
                initSubElements(self, getMatch(self, selector), selector);
            });
        }
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            tokens: [],
            imageEl: null,
            actions: null,
            appliedStyleCache: {}
        },

        /* imports added at runtime */
        imports: {
        },

        /* array of custom event types fired by this control */
        events: ['click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imageEl = shmi.getUiElement('image', self.element);

                if (!self.vars.imageEl) {
                    shmi.log("[iq-dynamic-svg-image] Missing image element", 3);
                    return;
                }

                self.vars.imageEl.setAttribute('draggable', 'false');

                if (self.config["image-src"]) {
                    shmi.loadResource(self.config["image-src"], function(response, err) {
                        if (err) {
                            console.error("Request failed to load: " + self.config['image-src']);
                        } else {
                            self.vars.imageEl.innerHTML = response + self.vars.imageEl.innerHTML;
                            rebuildViewBoxIfNeeded(self);
                            initializeImageListenersAndCache(self);
                            if (self.active) {
                                self.vars.listeners.forEach(function(l) {
                                    l.enable();
                                });
                            }
                        }
                    });
                } else {
                    console.error("[iq-dynamic-svg-image] Image source not configured");
                    return;
                }

                if (self.config['image-alt']) {
                    self.vars.imageEl.setAttribute('title', self.config['image-alt']);
                }
                applyScalingMode(self);
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this,
                    subscribedItems = [];
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                if (Array.isArray(self.config.selectors)) {
                    self.config.selectors.forEach(function(selector, idx) {
                        if (selector && Array.isArray(selector.selectorValue.valueMapping)) {
                            selector.selectorValue.valueMapping.forEach(function(subval) {
                                if (subval.item && !subscribedItems.includes(subval.item)) {
                                    self.vars.tokens.push(shmi.visuals.session.ItemManager.subscribeItem(subval.item, self));
                                    subscribedItems.push(subval.item);
                                }
                            });
                        }
                    });
                }
                shmi.log("[iq-dynamic-svg-image] Enabled", 1);
            },
            onSetValue: function(value, type, name) {
                const self = this;

                const operatorFuncs = {
                    'eq': function(a, b) {
                        return a === b;
                    },
                    'neq': function(a, b) {
                        return a !== b;
                    },
                    'gt': function(a, b) {
                        return a > b;
                    },
                    'lt': function(a, b) {
                        return a < b;
                    },
                    'geq': function(a, b) {
                        return a >= b;
                    },
                    'leq': function(a, b) {
                        return a <= b;
                    },
                    'and': function(a, b) {
                        return ((a & b) !== 0);
                    },
                    'or': function(a, b) {
                        return ((a | b) !== 0);
                    },
                    'xor': function(a, b) {
                        return ((a ^ b) !== 0);
                    },
                    'nand': function(a, b) {
                        return ((a & b) === 0);
                    },
                    'nor': function(a, b) {
                        return ((a | b) === 0);
                    }
                };

                self.config.selectors.forEach(function(selector, idx) {
                    if (selector.selectorValue && Array.isArray(selector.selectorValue.valueMapping)) {
                        selector.selectorValue.valueMapping.forEach(function(subval, subidx) {
                            if (name === subval.item) {
                                removeStyle(self, selector, getMatch(self, selector), name, idx, subidx);
                            }
                        });
                    }
                });
                self.config.selectors.forEach(function(selector, idx) {
                    if (selector.selectorValue && Array.isArray(selector.selectorValue.valueMapping)) {
                        selector.selectorValue.valueMapping.forEach(function(subval, subidx) {
                            if (name === subval.item) {
                                const cmpVal = subval.itemValue;
                                if (subval.operator && operatorFuncs[subval.operator](value, cmpVal)) {
                                    applyStyle(self, selector, getMatch(self, selector), name, subval, idx, subidx);
                                }
                            }
                        });
                    }
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                shmi.log("[iq-dynamic-svg-image] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                shmi.log("[iq-dynamic-svg-image] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[iq-dynamic-svg-image] unlocked", 1);
            }
        }
    };
    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-flip-switch
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-flip-switch",
 *     "name": null,
 *     "template": "default/iq-flip-switch"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 * "on-value": The ON value
 * "off-value": The OFF value
 * "transitionStyle": Legacy
 * "confirm-off-text": OFF confirmation text
 * "confirm-on-text": ON confirmation text
 * "confirm-on": Whether to confirm ON
 * "confirm-off": Whether to confirm OFF
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-flip-switch", // control name in camel-case
        uiType = "iq-flip-switch", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-flip-switch iq-variant-01",
        "name": null,
        "template": "default/iq-flip-switch.iq-variant-01",
        "label": '[Label]',
        "auto-label": true,
        "on-label": null,
        "off-label": null,
        "item": null,
        "show-icon": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "on-value": 1,
        "off-value": 0,
        "transitionStyle": "all .16s linear",
        "confirm-off-text": "${V_CONFIRM_OFF}",
        "confirm-on-text": "${V_CONFIRM_ON}",
        "confirm-on": false,
        "confirm-off": false,
        "on-show-icon": false,
        "on-icon-src": "pics/system/controls/iq-flip-switch/flip-on.svg",
        "on-icon-class": null,
        "on-tooltip": null,
        "off-show-icon": false,
        "off-icon-src": "pics/system/controls/iq-flip-switch/flip-off.svg",
        "off-icon-class": null,
        "off-tooltip": null,
        "inline-svg-icon": true
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            shmi.addClass(self.element, "no-label");
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Updates the handle position based on the widgets current value.
     *
     * @param {*} self Reference to the widget
     */
    function updateHandle(self) {
        const value = self.getValue(),
            handleWidth = self.vars.handleEl ? Math.ceil(self.vars.handleEl.getBoundingClientRect().width) : 0,
            boxWidth = self.vars.handleBoxEl ? self.vars.handleBoxEl.clientWidth : 0;

        if (self.vars.isDragging || !self.vars.handleBoxEl) {
            return;
        }

        if (value === self.config['on-value']) {
            self.vars.handleDragOffset = (boxWidth - handleWidth);
        } else {
            self.vars.handleDragOffset = 0;
        }
    }

    /**
     * Creates a listener for the `window.resize` event.
     *
     * @param {*} self Reference to the widget
     * @param {function} handlerFunc Event handler
     * @returns {object}
     */
    function makeResizeListener(self, handlerFunc) {
        return {
            disable: () => window.removeEventListener("resize", handlerFunc),
            enable: () => window.addEventListener("resize", handlerFunc)
        };
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            iconEl: null,
            labelEl: null,
            labelOnEl: null,
            labelOffEl: null,
            handleEl: null,
            handleBoxEl: null,
            handleIconEl: null,

            initialized: false,
            subscriptionTargetId: null,
            isDragging: false,
            width: null,
            handleWidth: null,
            handleDragOffset: 0
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.labelOnEl = shmi.getUiElement('label-on', self.element);
                self.vars.labelOffEl = shmi.getUiElement('label-off', self.element);
                self.vars.handleEl = shmi.getUiElement('handle', self.element);
                self.vars.handleBoxEl = shmi.getUiElement('handle-box', self.element);
                self.vars.handleIconEl = shmi.getUiElement('handle-icon', self.element);

                // Validate req'd elements
                if (!self.vars.handleEl || !self.vars.handleBoxEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-flip-switch] Handle or handle box element is missing!", 3);
                    self.vars.labelEl = null;
                    self.vars.labelOnEl = null;
                    self.vars.labelOffEl = null;
                    self.vars.handleEl = null;
                    self.vars.handleBoxEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-flip-switch] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                /**************/
                /*** LABELS ***/
                /**************/
                setLabelImpl(self, self.config.label);
                const value = self.getValue();

                if (self.vars.labelOnEl && self.config['on-label']) {
                    self.vars.labelOnEl.textContent = shmi.localize(self.config['on-label']);
                    if (self.config['on-value'] !== value) {
                        shmi.addClass(self.vars.labelOnEl, "no-label");
                    } else {
                        shmi.removeClass(self.vars.labelOnEl, "no-label");
                    }
                }

                if (self.vars.labelOffEl && self.config['off-label']) {
                    self.vars.labelOffEl.textContent = shmi.localize(self.config['off-label']);
                    if (self.config['off-value'] !== value) {
                        shmi.addClass(self.vars.labelOffEl, "no-label");
                    } else {
                        shmi.removeClass(self.vars.labelOffEl, "no-label");
                    }
                }

                /*******************/
                /*** HANDLE-ICON ***/
                /*******************/

                if (!self.vars.handleIconEl) {
                    shmi.log('[IQ:iq-flip-switch] no handle-icon element provided', 1);
                }

                if (value === self.config['on-value']) {
                    if (self.config['on-icon-src'] && self.config['on-show-icon']) {
                        applyIcon(self.vars.handleIconEl, self.config["on-icon-src"], null, self.config["inline-svg-icon"]);
                        shmi.addClass(self.element, 'has-handle-icon');
                    } else if (self.config['on-icon-class'] && self.config['on-show-icon']) {
                        applyIcon(self.vars.handleIconEl, null, self.config["on-icon-class"]);
                        shmi.addClass(self.element, 'has-handle-icon');
                    } else {
                        shmi.removeClass(self.element, 'has-handle-icon');
                    }
                }

                if (value === self.config['off-value']) {
                    if (self.config['off-icon-src'] && self.config['off-show-icon']) {
                        applyIcon(self.vars.handleIconEl, self.config["off-icon-src"], null, self.config["inline-svg-icon"]);
                        shmi.addClass(self.element, 'has-handle-icon');
                    } else if (self.config['off-icon-class'] && self.config['off-show-icon']) {
                        applyIcon(self.vars.handleIconEl, null, self.config["off-icon-class"]);
                        shmi.addClass(self.element, 'has-handle-icon');
                    } else {
                        shmi.removeClass(self.element, 'has-handle-icon');
                    }
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                let handleDragAfId = null;
                // Handle element listeners
                var handleEventFunctions = {
                    onDrag: function(dx, dy, e) {
                        const handleWidth = self.vars.handleEl ? Math.ceil(self.vars.handleEl.getBoundingClientRect().width) : 0,
                            boxWidth = self.vars.handleBoxEl ? self.vars.handleBoxEl.clientWidth : 0;

                        e.preventDefault();
                        if (self.vars.handleDragOffset + dx < 0) {
                            self.vars.handleDragOffset = 0;
                        } else if (self.vars.handleDragOffset + dx + handleWidth > boxWidth) {
                            self.vars.handleDragOffset = boxWidth - handleWidth;
                        } else {
                            self.vars.handleDragOffset += dx;
                        }

                        if (handleDragAfId) {
                            shmi.caf(handleDragAfId);
                        }

                        handleDragAfId = shmi.raf(() => {
                            self.vars.handleEl.style.transform = `left(${self.vars.handleDragOffset}px)`;
                        });

                        if (!self.vars.isDragging) {
                            self.vars.isDragging = true;
                        }
                    },
                    onPress: function() {
                        shmi.addClass(self.vars.handleEl, 'pressed');
                    },
                    onRelease: function() {
                        if (self.vars.isDragging) {
                            self.vars.isDragging = false;
                            if (self.vars.handleDragOffset > ((self.vars.handleBoxEl.offsetWidth - self.vars.handleEl.offsetWidth) / 2)) {
                                if (self.config['confirm-on']) {
                                    shmi.confirm(self.config['confirm-on-text'], function(confirmed) {
                                        self.handleOnOff(true, confirmed);
                                    });
                                } else {
                                    self.handleOnOff(true, null);
                                }
                            } else if (self.config['confirm-off']) {
                                shmi.confirm(self.config['confirm-off-text'], function(confirmed) {
                                    self.handleOnOff(false, confirmed);
                                });
                            } else {
                                self.handleOnOff(false, null);
                            }
                        }

                        if (handleDragAfId) {
                            shmi.caf(handleDragAfId);
                        }
                        self.vars.handleEl.style.transform = '';
                        shmi.removeClass(self.vars.handleEl, 'pressed');
                    },
                    onClick: function() {
                        if (self.vars.value === self.config['on-value']) {
                            if (self.config['confirm-off']) {
                                shmi.confirm(self.config['confirm-off-text'], function(conf) {
                                    if (conf) {
                                        self.turnOnOff(false);
                                    }
                                });
                            } else {
                                self.turnOnOff(false);
                            }
                        } else if (self.config['confirm-on']) {
                            shmi.confirm(self.config['confirm-on-text'], function(conf) {
                                if (conf) {
                                    self.turnOnOff(true);
                                }
                            });
                        } else {
                            self.turnOnOff(true);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.handleEl, handleEventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.handleEl, handleEventFunctions));

                // Handle box listeners
                var handleBoxEventFunctions = {
                    onClick: handleEventFunctions.onClick
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.handleBoxEl, handleBoxEventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.handleBoxEl, handleBoxEventFunctions));

                self.vars.listeners.push(makeResizeListener(self, updateHandle.bind(null, self)));
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.log("[IQ:iq-flip-switch] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.subscriptionTargetId) {
                    self.vars.subscriptionTargetId.unlisten();
                    self.vars.subscriptionTargetId = null;
                }

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.log("[IQ:iq-flip-switch] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-flip-switch] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-flip-switch] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    oldValue = self.vars.value;

                if (self.vars.isDragging || !self.vars.handleBoxEl) {
                    return;
                }

                if (value === self.config['on-value']) {
                    self.vars.value = self.config['on-value'];
                    shmi.addClass(self.element, 'on');

                    if (!self.vars.handleIconEl || !self.config['on-show-icon'] || !(self.config['on-icon-class'] || self.config['on-icon-src'])) {
                        shmi.removeClass(self.element, 'has-handle-icon');
                    } else {
                        shmi.addClass(self.element, 'has-handle-icon');
                        self.setupIconElement(self.vars.handleIconEl, self.config['on-icon-class'], self.config['on-icon-src'], self.config["inline-svg-icon"]);
                    }

                    shmi.addClass(self.vars.labelOffEl, "no-label");
                    shmi.removeClass(self.vars.labelOnEl, "no-label");
                } else {
                    self.vars.value = self.config['off-value'];
                    shmi.removeClass(self.element, 'on');

                    if (!self.vars.handleIconEl || !self.config['off-show-icon'] || !(self.config['off-icon-class'] || self.config['off-icon-src'])) {
                        shmi.removeClass(self.element, 'has-handle-icon');
                    } else {
                        shmi.addClass(self.element, 'has-handle-icon');
                        self.setupIconElement(self.vars.handleIconEl, self.config['off-icon-class'], self.config['off-icon-src'], self.config["inline-svg-icon"]);
                    }

                    shmi.addClass(self.vars.labelOnEl, "no-label");
                    shmi.removeClass(self.vars.labelOffEl, "no-label");
                }

                if (oldValue !== self.vars.value) {
                    self.fire("change", {
                        value: self.vars.value
                    });

                    if (self.onChange) {
                        self.onChange(self.vars.value);
                    }
                }

                updateHandle(self);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /* Handles ON/OFF dragging */
            handleOnOff: function(turnOn, confirmed) {
                var self = this,
                    newValue = turnOn ? self.config['on-value'] : self.config['off-value'];

                // Only do this when it has been confirmed or no confirmation was enabled
                if (confirmed === true || confirmed === null) {
                    updateHandle(self);

                    if (self.config.item) {
                        shmi.visuals.session.ItemManager.writeValue(self.config.item, newValue);
                    } else {
                        self.setValue(newValue);
                    }
                } else {
                    // Reset to previous value as not confirmed
                    self.setValue(self.vars.value);
                }
            },

            /* Handles ON/OFF click */
            turnOnOff: function(turnOn) {
                var self = this,
                    newValue = turnOn ? self.config['on-value'] : self.config['off-value'];

                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, newValue);
                } else {
                    self.setValue(newValue);
                }
            },

            /**
             * Sets up the icon depending on the configuration
             *
             * @param {object} iconEl
             * @param {string} iconClass
             * @param {string} iconSrc
             * @param {bool} inlineSvg
             */
            setupIconElement: function(iconEl, iconClass, iconSrc, inlineSvg) {
                const { applyIcon } = shmi.requires("visuals.tools.icons");

                if (iconClass) {
                    applyIcon(iconEl, null, iconClass);
                } else if (iconSrc) {
                    applyIcon(iconEl, iconSrc, null, inlineSvg);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-heartbeat
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-heartbeat",
 *     "name": null,
 *     "template": "custom/controls/iq-heartbeat"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "item": The item
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iqHeartbeat", // control name in camel-case
        uiType = "iq-heartbeat", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-heartbeat iq-variant-01",
        "name": null,
        "template": "default/iq-heartbeat.iq-variant-01",
        "item": null,
        "item-plc": null,
        "timeout-plc": null,
        "image-ok-src": null,
        "image-ok-title": null,
        "image-fail-src": null,
        "image-fail-title": null,
        "action-fail": null,
        "action-ok": null,
        "item-hmi": null,
        "timeout-hmi": null,
        "scaling-mode": "fit-height"
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Handles the set scaling mode
     *
     * @param {object} self
     */
    function applyScalingMode(self) {
        let scalingMode = "fit-height";

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "stretch":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        shmi.addClass(self.element, scalingMode);
    }

    /**
     * Tries as long as required until the update is successful
     *
     * @param self
     * @param oldVal
     * @param newVal
     * @param callback
     */
    function performCompareExchange(self, oldVal, newVal, callback) {
        if (self.vars.isEnabled) {
            self.imports.im.compareExchange(self.config['item-hmi'], oldVal, newVal, function(expectedValue, retry, error) {
                if (error && retry) {
                    newVal = expectedValue + 1;
                    if (newVal > Number.MAX_SAFE_INTEGER) {
                        newVal = 0;
                    }

                    performCompareExchange(self, expectedValue, newVal, callback);
                } else if (typeof callback === 'function') {
                    callback(!error);
                }
            });
        }
    }

    /**
     * called if plc item timer elapses
     *
     * @param {object} self control instance
     */
    function onPlcTimerElapsed(self) {
        self.vars.isDisconnected = true;
        self.vars.imgEl.src = self.vars.imagePathFail;
        self.setTooltip(self.config['image-fail-title']);
        if (self.vars.actionFail) {
            self.vars.actionFail.execute();
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            active: false,

            // DOM Elements
            imgEl: null,

            tokens: [],
            plcItemValue: null,
            plcItemTimer: null,
            hmiItemValue: null,
            hmiItemTimer: null,
            hmiItemTimerActive: false,
            isDisconnected: false,
            actionOk: null,
            actionFail: null,

            // Images
            imagePathOk: null,
            imagePathFail: null,

            isEnabled: false
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imgEl = shmi.getUiElement('image', self.element);
                if (!self.vars.imgEl) {
                    self.log('Image element is missing!', 3);
                    return;
                }

                // Images from layout template - allows us to use different images per layout
                if (self.vars.imgEl.dataset.imgOk && self.vars.imgEl.dataset.imgFail) {
                    self.vars.imagePathOk = self.vars.imgEl.dataset.imgOk;
                    self.vars.imagePathFail = self.vars.imgEl.dataset.imgFail;
                }

                // Override images if defined by user
                if (self.config['image-ok-src']) {
                    self.vars.imagePathOk = self.config['image-ok-src'];
                }
                if (self.config['image-fail-src']) {
                    self.vars.imagePathFail = self.config['image-fail-src'];
                    self.setTooltip(self.config['image-fail-title']);
                }

                self.vars.imgEl.src = self.vars.imagePathFail; // Initial status

                // UI Actions
                if (self.config['action-fail']) {
                    self.vars.actionFail = new shmi.visuals.core.UiAction(self.config['action-fail'], self);
                }
                if (self.config['action-ok']) {
                    self.vars.actionOk = new shmi.visuals.core.UiAction(self.config['action-ok'], self);
                }

                applyScalingMode(self);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.isEnabled = true;

                /**************************/
                /*** PLC ITEM (MONITOR) ***/
                /**************************/
                if (self.config['item-plc']) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["item-plc"], self));

                    // Start timer
                    self.vars.plcItemTimer = setTimeout(onPlcTimerElapsed.bind(null, self), self.config['timeout-plc']);
                }

                /************************/
                /*** HMI ITEM (WRITE) ***/
                /************************/
                if (self.config['item-hmi'] && self.config['timeout-hmi']) {
                    let hmiHandler = self.imports.im.getItemHandler();
                    hmiHandler.setValue = function(value) {
                        self.vars.hmiItemValue = parseInt(value);
                    };
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["item-hmi"], hmiHandler));

                    // Start timer for HMI item update
                    self.vars.hmiItemTimer = setInterval(function() {
                        if (self.vars.hmiItemTimerActive) {
                            return;
                        }
                        self.vars.hmiItemTimerActive = true;

                        // Increment the value
                        let newVal = self.vars.hmiItemValue;
                        if (newVal === null) {
                            newVal = 0;
                        }
                        if (newVal === Number.MAX_VALUE) {
                            newVal = 0;
                        }

                        newVal += 1;

                        // We intentionally do not monitor the result as we have no way to do anything with it
                        performCompareExchange(self, self.vars.hmiItemValue, newVal, function(success) {
                            self.vars.hmiItemTimerActive = false;
                        });
                    }, self.config['timeout-hmi']);
                }

                self.log('Enabled', 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.isEnabled = false;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                // HMI -> PLC
                if (self.vars.hmiItemTimer) {
                    clearInterval(self.vars.hmiItemTimer);
                    self.vars.hmiItemTimer = null;
                }

                // PLC -> HMI
                if (self.vars.plcItemTimer) {
                    clearTimeout(self.vars.plcItemTimer);
                    self.vars.plcItemTimer = null;
                }

                self.log('Disabled', 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value) {
                var self = this;

                // Don't do anything if no plc timeout is set.
                if (!self.config['timeout-plc']) {
                    return;
                }

                // Stop the running timer as we just got an update!
                if (self.vars.plcItemTimer) {
                    clearTimeout(self.vars.plcItemTimer);
                    self.vars.plcItemTimer = null;
                }

                // Have we been disconnected before?
                if (self.vars.isDisconnected) {
                    self.vars.isDisconnected = false;
                    if (self.vars.actionOk) {
                        self.vars.actionOk.execute();
                    }
                }

                if (self.vars.imgEl.src !== self.vars.imagePathOk) {
                    self.setTooltip(self.config['image-ok-title']);
                    self.vars.imgEl.src = self.vars.imagePathOk;
                }

                //restart timer
                self.vars.plcItemTimer = setTimeout(onPlcTimerElapsed.bind(null, self), self.config['timeout-plc']);
            },

            log: function(msg, level) {
                shmi.log('[IQ:iq-heartbeat] ' + msg, level);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-image-changer
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-image-changer",
 *     "name": null,
 *     "template": "custom/controls/iq-image-changer"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "item": The item
 * "tooltip": Tooltip
 * "options": Array of options
 * "default-image": The default image to show when none has been configured yet
 * "default-title": The default title
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-image-changer", // control name in camel-case
        uiType = "iq-image-changer", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-image-changer iq-variant-01",
        "name": null,
        "template": "default/iq-image-changer.iq-variant-01",
        "item": null,
        "tooltip": null,
        "options": [],
        "default-image": "pics/system/icons/placeholder.svg",
        "default-title": null,
        "scaling-mode": "fit-height",
        "action": null,
        "action-release": null,
        "action-while-pressed": null,
        "interval-while-pressed": null,
        "action-pressed": null,
        "write-bool": false,
        "no-initial-image": false,
        "on-value": 1,
        "off-value": 0,
        "inline-svg": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * apply image scaling selector according to configuration
     *
     * @param {object} self instance reference
     */
    function applyScalingMode(self) {
        let scalingMode = "fit-height";

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "stretch":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        shmi.addClass(self.element, scalingMode);
    }

    /**
     * ensure that correct element type for displayed image is present
     *
     * @param {object} self instance reference
     * @param {string} [tagName="IMG"] element tag name
     */
    function ensureImageElement(self, tagName = "IMG") {
        const { imageEl } = self.vars;

        if (!imageEl) {
            return;
        }

        if (imageEl.tagName !== tagName) {
            const newElement = document.createElement(tagName);
            newElement.setAttribute("data-ui", "image");
            newElement.classList.add("iqel-image");

            imageEl.replaceWith(newElement);

            self.vars.imageEl = newElement;
        }
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            imageEl: null,
            action: null,
            actionRelease: null,
            actionWhilePressed: null,
            actionWhilePressedTimer: null,
            mouseListener: null,
            touchListener: null,

            activeOption: null,
            subscriptionTargetId: null,
            loadPromise: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: ['press', 'release', 'click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.loadPromise = Promise.resolve();

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imageEl = shmi.getUiElement('image', self.element);

                // UI Actions
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }
                if (self.config['action-press']) {
                    self.vars.actionPress = new shmi.visuals.core.UiAction(self.config['action-press'], self);
                }
                if (self.config['action-release']) {
                    self.vars.actionRelease = new shmi.visuals.core.UiAction(self.config['action-release'], self);
                }
                if (self.config['action-while-pressed']) {
                    self.vars.actionWhilePressed = new shmi.visuals.core.UiAction(self.config['action-while-pressed'], self);
                }

                if (Array.isArray(self.config.action) && self.config.action.length > 0) {
                    shmi.addClass(self.vars.imageEl, 'clickable');
                } else {
                    shmi.removeClass(self.vars.imageEl, 'clickable');
                }

                applyScalingMode(self);

                if (!self.config['no-initial-image']) {
                    self.getActive(null);
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                const session = shmi.visuals.session,
                    eventFunctions = {
                        onClick: function(x, y, e) {
                            if (session.FocusElement !== null) {
                                session.FocusElement.blur();
                                session.FocusElement = null;
                            }
                            if (self.vars.imageEl instanceof HTMLElement) {
                                self.vars.imageEl.focus();
                                session.FocusElement = self.vars.imageEl;
                                shmi.log("[IQ:iq-image-changer] focused", 1);
                            } else {
                                shmi.log("[IQ:iq-image-changer] only HTMLElements may be focused, type: " + (self.vars.imageEl.constructor), 1);
                            }

                            self.fire("click", {
                                x: x,
                                y: y,
                                event: e
                            });

                            if (self.vars.action) {
                                self.vars.action.execute();
                            }
                        },
                        onRelease: function(x, y, e) {
                            self.fire('release', {
                                x: x,
                                y: y,
                                event: e
                            });
                            if (self.config['write-bool'] && self.config.item) {
                                self.writeValue(self.config["off-value"]);
                            }

                            // Stop any running timer on release
                            if (self.vars.actionWhilePressedTimer) {
                                clearInterval(self.vars.actionWhilePressedTimer);
                            }

                            if (self.vars.actionRelease) {
                                self.vars.actionRelease.execute();
                            }
                        },
                        onPress: function(x, y, e) {
                            self.fire('press', {
                                x: x,
                                y: y,
                                event: e
                            });

                            if (self.config['write-bool'] && self.config.item) {
                                self.writeValue(self.config["on-value"]);
                            }

                            // UI Action: onPress
                            if (self.vars.actionPress) {
                                self.vars.actionPress.execute();
                            }

                            // UI Action while pressed
                            if (self.vars.actionWhilePressed && self.config['interval-while-pressed']) {
                                if (self.vars.actionWhilePressedTimer) {
                                    clearInterval(self.vars.actionWhilePressedTimer);
                                }

                                // While pressed timer
                                self.vars.actionWhilePressedTimer = setInterval(function() {
                                    self.vars.actionWhilePressed.execute();
                                }, self.config['interval-while-pressed']);
                            }
                        }
                    };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.imageEl, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.imageEl, eventFunctions));
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.log("[IQ:iq-image-changer] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image-changer] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image-changer] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-image-changer] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this;

                self.vars.value = value;
                self.getActive(self.vars.value);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {
            },

            /**
             * Custom function
             *
             * @param {object} value
             */
            getActive: function(value) {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");
                let opt = value !== null ? self.config.options.find((o) => {
                    if (Number.isSafeInteger(value) && Number.isSafeInteger(o.mask) && Number.isSafeInteger(o.value)) {
                        return (BigInt(value) & BigInt(o.mask)) === BigInt(o.value);
                    }

                    return o.value === value;
                }) : null;

                if (!opt) {
                    opt = {
                        "icon-src": self.config["default-image"],
                        "label": self.config["default-title"] || null
                    };
                }
                const lastOption = self.vars.activeOption;
                self.vars.activeOption = opt;

                if (opt["icon-src"] && self.config["inline-svg"] && opt["icon-src"].toLocaleLowerCase().endsWith(".svg")) {
                    ensureImageElement(self, "DIV");

                    self.vars.loadPromise = self.vars.loadPromise.then(
                        () => applyIcon(self.vars.imageEl, opt["icon-src"], null, self.config["inline-svg"])
                    ).catch((err) => {
                        console.error("Error loading image resource:", err);
                    });
                } else {
                    ensureImageElement(self, "IMG");
                    self.vars.imageEl.setAttribute("src", opt["icon-src"]);
                }

                if (lastOption?.style && typeof lastOption.style === "object") {
                    Object.keys(lastOption.style).forEach((prop) => {
                        self.vars.imageEl.style[prop] = "";
                    });
                }

                if (opt.style && typeof opt.style === "object") {
                    Object.keys(opt.style).forEach((prop) => {
                        self.vars.imageEl.style[prop] = opt.style[prop];
                    });
                }
                self.setTooltip(self.getTooltip());
            },

            /**
             * Custom function
             *
             * @returns {null|definition.prototypeExtensions.activeOption.label}
             */
            getTooltip: function() {
                const self = this,
                    superTooltip = shmi.visuals.core.BaseControl.prototype.getTooltip.call(this);

                if (superTooltip) {
                    return superTooltip;
                } else if (self.vars.activeOption && self.vars.activeOption.label) {
                    return self.vars.activeOption.label;
                }

                return null;
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-image
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-image",
 *     "name": null,
 *     "template": "custom/controls/iq-image"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "tooltip": Tooltip
 * "image-src": The image source
 *  "image-title": The title
 *  "mg-alt": The alt text
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-image", // control name in camel-case
        uiType = "iq-image", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-image iq-variant-01",
        "name": null,
        "template": "default/iq-image.iq-variant-01",
        "tooltip": null,
        "image-src": "pics/system/icons/placeholder.svg",
        "image-alt": null,
        "image-title": null,
        "scaling-mode": "fit-height",
        "action": null,
        "action-release": null,
        "action-while-pressed": null,
        "interval-while-pressed": null,
        "action-pressed": null,
        "write-bool": false,
        "on-value": 1,
        "off-value": 0,
        "inline-svg": true
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * apply image scaling selector according to configuration
     *
     * @param {object} self instance reference
     */
    function applyScalingMode(self) {
        let scalingMode = "fit-height";

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "stretch":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        shmi.addClass(self.element, scalingMode);
    }

    /**
     * ensure that correct element type for displayed image is present
     *
     * @param {object} self instance reference
     * @param {string} [tagName="IMG"] element tag name
     */
    function ensureImageElement(self, tagName = "IMG") {
        const { imageEl } = self.vars;

        if (!imageEl) {
            return;
        }

        if (imageEl.tagName !== tagName) {
            const newElement = document.createElement(tagName);
            newElement.setAttribute("data-ui", "image");
            newElement.classList.add("iqel-image");

            imageEl.replaceWith(newElement);

            self.vars.imageEl = newElement;
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            imageEl: null,
            action: null,
            actionRelease: null,
            actionWhilePressed: null,
            actionWhilePressedTimer: null,
            mouseListener: null,
            touchListener: null
        },

        /* imports added at runtime */
        imports: {
        },

        /* array of custom event types fired by this control */
        events: ['press', 'release', 'click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imageEl = shmi.getUiElement('image', self.element);

                if (!self.vars.imageEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-image] Missing image element", 3);
                    return;
                }

                self.vars.imageEl.setAttribute('draggable', 'false');
                if (self.config['image-src']) {
                    if (self.config["inline-svg"] && self.config['image-src'].toLocaleLowerCase().endsWith(".svg")) {
                        ensureImageElement(self, "DIV");
                        applyIcon(self.vars.imageEl, self.config["image-src"], null, true);
                    } else {
                        ensureImageElement(self, "IMG");
                        self.vars.imageEl.setAttribute('src', self.config['image-src']);
                    }
                }
                if (self.config['image-alt']) {
                    self.vars.imageEl.setAttribute('alt', self.config['image-alt']);
                }

                // UI Actions
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }
                if (self.config['action-press']) {
                    self.vars.actionPress = new shmi.visuals.core.UiAction(self.config['action-press'], self);
                }
                if (self.config['action-release']) {
                    self.vars.actionRelease = new shmi.visuals.core.UiAction(self.config['action-release'], self);
                }
                if (self.config['action-while-pressed']) {
                    self.vars.actionWhilePressed = new shmi.visuals.core.UiAction(self.config['action-while-pressed'], self);
                }

                if (Array.isArray(self.config.action) && self.config.action.length > 0) {
                    shmi.addClass(self.vars.imageEl, 'clickable');
                } else {
                    shmi.removeClass(self.vars.imageEl, 'clickable');
                }

                applyScalingMode(self);

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                const session = shmi.visuals.session;
                var eventFunctions = {
                    onClick: function(x, y, e) {
                        if (session.FocusElement !== null) {
                            session.FocusElement.blur();
                            session.FocusElement = null;
                        }
                        if (self.vars.imageEl instanceof HTMLElement) {
                            self.vars.imageEl.focus();
                            session.FocusElement = self.vars.imageEl;
                            shmi.log("[IQ:iq-image] focused", 1);
                        } else {
                            shmi.log("[IQ:iq-image] only HTMLElements may be focused, type: " + (self.vars.imageEl.constructor), 1);
                        }

                        self.fire("click", {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }
                    },
                    onRelease: function(x, y, e) {
                        self.fire('release', {
                            x: x,
                            y: y,
                            event: e
                        });
                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["off-value"]);
                        }

                        // Stop any running timer on release
                        if (self.vars.actionWhilePressedTimer) {
                            clearInterval(self.vars.actionWhilePressedTimer);
                        }

                        if (self.vars.actionRelease) {
                            self.vars.actionRelease.execute();
                        }
                    },
                    onPress: function(x, y, e) {
                        self.fire('press', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["on-value"]);
                        }

                        // UI Action: onPress
                        if (self.vars.actionPress) {
                            self.vars.actionPress.execute();
                        }

                        // UI Action while pressed
                        if (self.vars.actionWhilePressed && self.config['interval-while-pressed']) {
                            if (self.vars.actionWhilePressedTimer) {
                                clearInterval(self.vars.actionWhilePressedTimer);
                            }

                            // While pressed timer
                            self.vars.actionWhilePressedTimer = setInterval(function() {
                                self.vars.actionWhilePressed.execute();
                            }, self.config['interval-while-pressed']);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.imageEl, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.imageEl, eventFunctions));
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.log("[IQ:iq-image] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-image] unlocked", 1);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-input-field
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-input-field",
 *     "name": null,
 *     "template": "default/iq-input-field"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-unit-text": Whether to use the auto-unit (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "numpad-enabled": Whether the numpad is enabled
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqInputField", // control name in camel-case
        uiType = "iq-input-field", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": `${uiType} iq-variant-01`,
        "name": null,
        "template": "default/iq-input-field.iq-variant-01",
        "label": '[Label]',
        "placeholder": null,
        "item": null,
        "numeric-class": "numeric",
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "step": 1,
        "precision": -1,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "type": shmi.c("TYPE_STRING"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "unit-scale": 1,
        "auto-min": true,
        "auto-max": true,
        "auto-step": true,
        "auto-precision": true,
        "auto-type": true,
        "numpad-enabled": false,
        "show-icon": true,
        "icon-src": null,
        "inline-svg-icon": true,
        "icon-class": null,
        "tooltip": null,
        "multiline": false,
        "notResizable": false
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.unitEl) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined") {
            self.vars.unit = "";
            self.vars.unitEl.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.unitEl.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    function updateType(self) {
        const { TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING } = shmi.Constants,
            { type, inputEl } = self.vars;

        if (inputEl) {
            switch (type) {
            case TYPE_BOOL:
            case TYPE_INT:
                inputEl.setAttribute("inputmode", "numeric");
                break;
            case TYPE_FLOAT:
                inputEl.setAttribute("inputmode", "decimal");
                break;
            case TYPE_STRING:
            default:
                inputEl.setAttribute("inputmode", "text");
            }
        }
    }

    /**
     * Parses the incoming input to its intended value
     *
     * @param {*} self Reference to the widget
     * @param {?string} inputString Incoming input string
     */
    function parseValue(self, inputString) {
        if (self.config.multiline) {
            // Multi-line is always string, therefore no check and
            // formatting is performed on it.
            return inputString;
        } else if (![shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT"), shmi.c("TYPE_BOOL")].includes(self.vars.type)) {
            return inputString;
        } else if (!self.vars.floatRegexp.test(inputString.replace(shmi.localize("${DECIMAL_DELIMITER}"), '.'))) {
            return null;
        }

        return self.imports.nv.applyInputSettings(inputString, self);
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: null,
            active: false,

            // DOM Elements
            currentValueEl: null,
            labelEl: null,
            unitEl: null,
            iconEl: null,
            inputEl: null, // Either currentValueEl || textAreaEl depending on the config
            textAreaEl: null,

            label: null,

            // Legacy
            domListeners: [],
            subscriptionTargetId: null,
            toEnterTmr: 0,
            valueSettings: null,
            uiAction: null,
            focused: false,
            locked: false,
            floatRegexp: "(^[+-]?[0-9]([.][0-9]*)?$|^[+-]?[1-9]+[0-9]*([.][0-9]*)?$)",
            intRegexp: "(^[+-]?[0-9]$|^[+-]?[1-9]+[0-9]*$)",
            type: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ["change", "enter"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    c = shmi.Constants,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                self.vars.floatRegexp = new RegExp(self.vars.floatRegexp);

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.currentValueEl = shmi.getUiElement('input', self.element);
                self.vars.textareaEl = shmi.getUiElement('textarea', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.unitEl = shmi.getUiElement('unit', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                if (!self.vars.currentValueEl || !self.vars.textareaEl) {
                    shmi.log('[IQ:iq-input-field] no input and/or element in HTML', 3);
                    return;
                }

                // Multiline handling
                if (self.config.multiline) {
                    self.vars.inputEl = self.vars.textareaEl;
                    self.vars.currentValueEl.remove();
                    self.vars.textareaEl.rows = '1';
                } else {
                    self.vars.inputEl = self.vars.currentValueEl;
                    self.vars.textareaEl.remove();
                }
                self.vars.inputEl.style.display = 'block';
                if (typeof self.config.placeholder === "string") {
                    self.vars.inputEl.setAttribute("placeholder", shmi.localize(self.config.placeholder));
                }

                // Resizable handling
                if (self.config.notResizable) {
                    shmi.addClass(self.element, "not-resizable");
                } else {
                    shmi.removeClass(self.element, "not-resizable");
                }

                /* all required elements found */
                self.vars.inputEl.setAttribute('tabindex', '0');
                self.insertValue("");

                const type = parseInt(self.config.type);
                if (!isNaN(type)) {
                    self.vars.type = type;
                    updateType(self);
                }

                if (self.config.action) {
                    self.vars.uiAction = new shmi.visuals.core.UiAction(self.config.action, this);
                }

                if (!self.config['auto-type']) {
                    if ([c.TYPE_BOOL, c.TYPE_INT, c.TYPE_FLOAT].indexOf(self.vars.type) !== -1) {
                        shmi.addClass(self.element, self.config['numeric-class']);
                    }
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/

                // Custom DOM Listeners
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'focus', function(e) {
                    if (!self.vars.focus) {
                        self.handleFocus();
                    }
                }));
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'keypress', function(e) {
                    if (((e.keyCode === 13) && (!self.config.multiline)) || (e.keyCode === 9)) {
                        e.preventDefault();
                        window.getSelection().removeAllRanges();
                        self.vars.currentValueEl.blur();
                    }
                }));
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'keydown', function(e) {
                    const itemType = (self.config.item && self.config['auto-type']) ? self.vars.valueSettings.type : self.config.type,
                        delimiter = shmi.localize("${DECIMAL_DELIMITER}"),
                        pressedKey = e.key;
                    if (itemType === shmi.c("TYPE_FLOAT") && !pressedKey.match(/[0-9]|Backspace|Delete|Enter|Home|End|ArrowLeft|ArrowRight|ArrowUp|ArrowDown|-/)) {
                        if (pressedKey !== delimiter) {
                            e.preventDefault();
                        }
                    }
                }));
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'blur', function(e) {
                    if (!self.vars.focused) {
                        return;
                    }
                    shmi.log("[IQ:iq-input-field] blur event", 1);
                    window.getSelection().removeAllRanges();

                    self.validateAndSet(self.vars.inputEl);
                    self.vars.focused = false;
                    shmi.visuals.session.FocusElement = null;
                }));

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-input-field] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);
                if (self.config['unit-scale']) {
                    self.config['unit-scale'] = parseFloat(shmi.localize(self.config['unit-scale']));
                }

                // Label
                setLabelImpl(self, self.config.label);
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.inputEl.disabled = false;
                self.enableAllListeners();

                if (document.activeElement === self.vars.inputEl) {
                    self.vars.focused = true;
                }

                shmi.log("[IQ:iq-input-field] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                self.disableAllListeners();

                shmi.log("[IQ:iq-input-field] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.locked = true;
                self.vars.inputEl.disabled = true;

                shmi.addClass(self.element, 'locked');

                self.vars.inputEl.blur();
                self.disableAllListeners();

                shmi.log("[IQ:iq-input-field] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.locked = false;
                self.vars.inputEl.disabled = false;

                shmi.removeClass(self.element, 'locked');

                self.enableAllListeners();

                shmi.log("[IQ:iq-input-field] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    oldValue = self.vars.value;

                self.vars.value = value;

                self.insertValue(self.imports.nv.formatOutput(self.vars.value, self));
                if (document.activeElement === self.vars.inputEl) {
                    self.selectContent();
                }
                shmi.log("[IQ:iq-input-field] new value: " + self.vars.value, 0);
                if (self.vars.value !== oldValue) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;
                let type = self.vars.type;

                if (self.config.item && self.config['auto-type']) {
                    type = self.imports.im.items[self.config.item].type;
                }

                if (type === shmi.c("TYPE_INT")) {
                    return Math.round(self.vars.value / self.config['unit-scale']);
                } else if (type === shmi.c("TYPE_FLOAT")) {
                    return (self.vars.value / self.config['unit-scale']);
                } else {
                    return self.vars.value;
                }
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {
                const self = this,
                    c = shmi.Constants;

                if (self.config['auto-type']) {
                    self.vars.type = type;
                    updateType(self);
                    if ([c.TYPE_BOOL, c.TYPE_INT, c.TYPE_FLOAT].indexOf(type) !== -1) {
                        shmi.addClass(self.element, self.config['numeric-class']);
                    }
                }

                self.imports.nv.setProperties(self, arguments);
            },

            setUnitText: function(unitText) {
                const self = this;

                if (self.vars.unitEl && self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText, false);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText, false);
                }
            },

            /**
             * Writes current value to connected data source
             *
             */
            updateValue: function() {
                if (this.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(this.config.item, this.vars.value);
                }
            },

            /**
             * Legacy
             *
             * @param value
             */
            insertValue: function(value) {
                const self = this;
                self.vars.inputEl.value = value;
            },

            /**
             * Legacy
             *
             * @returns {string|*|string|*}
             */
            retrieveValue: function() {
                const self = this;
                return self.vars.inputEl.value;
            },

            /**
             * Legacy
             */
            selectContent: function() {
                const self = this;
                self.vars.inputEl.select();
            },

            /**
             * Legacy
             *
             * handleFocus - handle focus of input element when control is clicked or tabbed into
             */
            handleFocus: function() {
                const self = this;

                /* prevent focusing if control is locked */
                if (self.vars.locked) {
                    return;
                }

                if ((!self.vars.focused) && (shmi.visuals.session.FocusElement !== null)) {
                    shmi.visuals.session.FocusElement.blur();
                    shmi.visuals.session.FocusElement = null;
                }
                shmi.log("[IQ:iq-input-field] focused", 1);

                self.selectContent();

                if (self.showNumpad()) {
                    return;
                } else if (self.showKeyboard()) {
                    return;
                }

                self.vars.focused = true;
                self.vars.inputEl.focus();
                shmi.visuals.session.FocusElement = self.vars.inputEl;

                if (self.vars.toEnterTmr) {
                    clearTimeout(self.vars.toEnterTmr);
                }

                self.vars.toEnterTmr = setTimeout(function() {
                    self.fire("enter", {
                        value: self.vars.value
                    });
                }, 250);
            },

            /**
             * Legacy
             *
             * @return {boolean} if numpad will be shown
             */
            showNumpad: function() {
                const self = this,
                    nv = shmi.requires("visuals.tools.numericValues");

                if (!self.config["numpad-enabled"]) { //does nothing if not enabled
                    return false;
                }

                if (!(self.vars && self.vars.valueSettings)) {
                    //initialize value settings in case no item is configured
                    nv.initValueSettings(self);
                }

                const ucAdapter = shmi.requires("visuals.tools.unitClasses"),
                    item = shmi.visuals.session.ItemManager.getItem(self.getItem());
                let calculatedMin = self.vars.valueSettings.min,
                    calculatedMax = self.vars.valueSettings.max;
                if (item !== null && item.unitClass !== null) {
                    const ua = ucAdapter.getSelectedAdapter(item.unitClass);
                    if (!self.config["auto-min"] && ua !== null) {
                        calculatedMin = ua.outFunction(self.vars.valueSettings.min);
                    }
                    if (!self.config["auto-max"] && ua !== null) {
                        calculatedMax = ua.outFunction(self.vars.valueSettings.max);
                    }
                }

                shmi.numpad(
                    {
                        "decimal-delimiter": self.config["decimal-delimiter"],
                        "unit": self.vars.unitEl ? self.vars.unitEl.textContent : self.config["unit-text"],
                        "label": self.vars.labelEl ? self.vars.labelEl.textContent : self.config.label,
                        "value": self.retrieveValue(),
                        "callback": function(res) {
                            if (self.config.item) {
                                if (self.config.multiline) {
                                    res = res.toString();
                                }
                                self.imports.im.writeValue(self.config.item, res);
                            } else {
                                self.setValue(res);
                            }
                        },
                        "min": calculatedMin,
                        "max": calculatedMax,
                        "type": self.vars.valueSettings.type,
                        "precision": self.vars.valueSettings.precision,
                        "item": (typeof self.config.item === "string" && self.config.item.length > 0) ? self.config.item : null
                    }
                );

                return true;
            },

            /**
             * Legacy
             *
             * @return {boolean} if keyboard will be shown
             */
            showKeyboard: function() {
                const self = this,
                    appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                if (!keyboardEnabled) {
                    return false;
                }

                shmi.keyboard(
                    {
                        "value": self.retrieveValue(),
                        "select-box-enabled": appConfig.keyboard["language-selection"],
                        "password-input": self.vars.currentValueEl && self.vars.currentValueEl.type && self.vars.currentValueEl.type.toLowerCase() === "password",
                        "show-enter": self.config.multiline,
                        "callback": function(success, input) {
                            if (success) {
                                input = parseValue(self, input);
                                if (input === null) {
                                    //Invalid input
                                } else if (self.config.item) {
                                    self.imports.im.writeValue(self.config.item, input);
                                } else {
                                    self.setValue(input);
                                }
                            }
                        }
                    });

                return true;
            },

            /**
             * Validates the content of the element and sets the value accordingly
             */
            validateAndSet: function() {
                const self = this,
                    inputString = self.retrieveValue(),
                    oldValue = self.vars.value;

                if (inputString === oldValue) {
                    return;
                }

                const newValue = parseValue(self, inputString) ?? oldValue;

                self.vars.value = newValue;
                if (typeof newValue === "undefined" || newValue === null) {
                    self.insertValue("");
                } else {
                    self.insertValue(self.imports.nv.formatOutput(newValue, self));
                }

                // Emit change event, write item and call UI actions only when something has changed
                if (newValue !== oldValue) {
                    if (self.config.item) {
                        self.imports.im.writeValue(self.config.item, newValue);
                    }

                    // UI Action
                    if (self.vars.uiAction) {
                        self.vars.uiAction.execute(newValue);
                    }

                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Helper functions
             */
            enableAllListeners: function() {
                const self = this;
                self.vars.domListeners.forEach(function(obj) {
                    obj.element.addEventListener(obj.event, obj.callback);
                });
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            disableAllListeners: function() {
                const self = this;
                self.vars.domListeners.forEach(function(obj) {
                    obj.element.removeEventListener(obj.event, obj.callback);
                });
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            createEventListener: function(element, event, callback) {
                return {
                    'event': event,
                    'callback': callback,
                    'element': element
                };
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-label
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-label",
 *     "name": null,
 *     "template": "custom/controls/iq-label"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "tooltip": Tooltip
 * "options": The options
 * "pattern": The pattern
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-label", // control name in camel-case
        uiType = "iq-label", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-label iq-variant-01",
        "name": null,
        "template": null,
        "item": null,
        "precision": -1,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "type": shmi.c("TYPE_INT"),
        "auto-precision": true,
        "auto-type": true,
        "tooltip": null,
        "options": [],
        "pattern": null,
        "value-as-tooltip": false,
        "text": "${label.no-value}",
        "item-display-mode": "value"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            value: 0,
            rafId: null,
            subscriptionTargetId: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                // Configure NV
                self.imports.nv.initValueSettings(self);

                if (self.config.text) {
                    self.updateText(shmi.localize(self.config.text));
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-label] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.vars.subscriptionTargetId) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-label] disabled", 1);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;
                return self.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision, tolMin, tolMax) {
                const self = this;
                self.imports.nv.setProperties(self, arguments);
                switch (self.config["item-display-mode"]) {
                case "min":
                    if (typeof min === "number") {
                        self.updateText(self.imports.nv.formatOutput(min, self));
                    }
                    break;
                case "max":
                    if (typeof max === "number") {
                        self.updateText(self.imports.nv.formatOutput(max, self));
                    }
                    break;
                case "prewarn-min":
                    if (typeof prewarnmin === "number") {
                        self.updateText(self.imports.nv.formatOutput(prewarnmin, self));
                    }
                    break;
                case "prewarn-max":
                    if (typeof prewarnmax === "number") {
                        self.updateText(self.imports.nv.formatOutput(prewarnmax, self));
                    }
                    break;
                case "warn-min":
                    if (typeof warnmin === "number") {
                        self.updateText(self.imports.nv.formatOutput(warnmin, self));
                    }
                    break;
                case "warn-max":
                    if (typeof warnmax === "number") {
                        self.updateText(self.imports.nv.formatOutput(warnmax, self));
                    }
                    break;
                case "tol-min":
                    if (typeof tolMin === "number") {
                        self.updateText(self.imports.nv.formatOutput(tolMin, self));
                    }
                    break;
                case "tol-max":
                    if (typeof tolMax === "number") {
                        self.updateText(self.imports.nv.formatOutput(tolMax, self));
                    }
                    break;
                default:
                }
            },

            /* Called by ItemManager when subscribed item changes and once on initial subscription
             *
             * "options" : [
             *  { "label" : "True",  "value" : 0 },
             *  { "label" : "False", "value" : 1 },
             *  { "label" : "=",     "value" : 2 },
             *  { "label" : ">",     "value" : 3 },
             *  { "label" : ">=",    "value" : 4 },
             *  { "label" : "<",     "value" : 5 },
             *  { "label" : "<=",    "value" : 6 },
             *  { "label" : "&",     "value" : 7 },
             *  { "label" : "|",     "value" : 8 },
             *  { "label" : "Null",  "value" : 9 }
             * ]
            **/
            onSetValue: function(value, type, name) {
                const self = this;

                if (self.config.item !== null &&
                    [
                        "label", "unit", "min", "max", "prewarn-min", "prewarn-max", "warn-min", "warn-max", "tol-min", "tol-max"
                    ].includes(self.config["item-display-mode"])
                ) {
                    return;
                }

                shmi.caf(self.vars.rafId);
                self.vars.rafId = shmi.raf(function() {
                    let newValue = value;

                    if (self.config.options.length) {
                        const selectedOption = self.config.options.find((o) => {
                            if (Number.isSafeInteger(value) && Number.isSafeInteger(o.mask) && Number.isSafeInteger(o.value)) {
                                return (BigInt(value) & BigInt(o.mask)) === BigInt(o.value);
                            }

                            return o.value === value;
                        });

                        if (selectedOption) {
                            newValue = shmi.localize(selectedOption.label);
                        }
                    } else if ((typeof self.config.pattern === "string") && self.config.pattern.trim().length) {
                        newValue = shmi.localize(shmi.evalString(self.config.pattern, { VALUE: value }));
                    } else {
                        newValue = self.imports.nv.formatOutput(value, self);
                    }

                    self.updateText(newValue);
                });
            },

            /**
             * Updates the text
             *
             * @param newValue
             */
            updateText: function(newValue) {
                const self = this;

                self.element.textContent = newValue;
                if (self.config["value-as-tooltip"]) {
                    self.setTooltip(newValue);
                }
            },
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            },
            setUnitText: function(unitText) {
                const self = this;

                if (self.config["item-display-mode"] === "unit") {
                    self.updateText(shmi.localize(unitText));
                }
            },
            setLabel: function(labelText) {
                const self = this;

                if (self.config["item-display-mode"] === "label") {
                    self.updateText(shmi.localize(labelText));
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-linear-gauge
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-linear-gauge",
 *     "template": "default/iq-linear-gauge.iq-variant-01",
 *     "name": "iq-linear-gauge",
 *     "ui": "iq-linear-gauge",
 *     "label": "[label]",
 *     "item": null,
 *     "min": null,
 *     "max": null,
 *     "unit-text": "[Unit]",
 *     "auto-label": true,
 *     "auto-unit-text": true,
 *     "auto-min": true,
 *     "auto-max": true,
 *     "type": 2,
 *     "auto-type": true,
 *     "decimal-delimiter": "${DECIMAL_DELIMITER}",
 *     "precision": 0,
 *     "auto-precision": true,
 *     "icon-src": null,
 *     "icon-class": null,
 *     "tooltip": null,
 *     "animation-duration": 100,
 *     "disable-alarm-styling": false,
 *     "rotation": 0,
 *     "default-value": "---",
 *     "show-value": true,
 *     "fill-inverse": false,
 *     "fill-reverse": false,
 *     "value-labels": 0
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * tooltip {string}: Tooltip
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iqLinearGauge", // control name in camel-case
        uiType = "iq-linear-gauge", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": uiType,
        "template": "default/iq-linear-gauge.iq-variant-01",
        "name": uiType,
        "ui": uiType,
        "label": "[label]",
        "item": null,
        "min": null,
        "max": null,
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "auto-min": true,
        "auto-max": true,
        "type": 2,
        "auto-type": true,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "precision": 0,
        "auto-precision": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "animation-duration": 100,
        "disable-alarm-styling": false,
        "rotation": 0,
        "default-value": "---",
        "show-value": true,
        "fill-inverse": false,
        "fill-reverse": false,
        "value-labels": 0
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${uiType}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${uiType}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${uiType}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${uiType}]`)
    };

    const TICK_LENGTH = 6,
        TICK_GAP = 8,
        TICK_WIDTH = 4,
        TICK_LABEL_DISTANCE = 6;

    /**
     * Returns a random id.
     *
     * @returns {string} random id
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.elements.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.elements.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.elements.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.elements.labelUnit) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined") {
            self.vars.elements.labelUnit.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            const localizedUnitText = shmi.localize(unitText);
            self.vars.elements.labelUnit.textContent = localizedUnitText;
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Ensures the cache object is present and some commonly used variables are
     * set.
     *
     * @param {*} self Reference to the control.
     */
    function checkCache(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }

        if (!self.vars.cache.gaugeWidth) {
            self.vars.cache.gaugeWidth = getStylePropertyValue(self.vars.elements.gaugePathFill, "width");
            self.vars.corners.outer = getStylePropertyValue(self.element, "--corner-radius-outer");
            self.vars.corners.inner = getStylePropertyValue(self.element, "--corner-radius-inner");

            if (self.vars.corners.outer > self.vars.cache.gaugeWidth / 2) {
                self.vars.corners.outer = self.vars.cache.gaugeWidth / 2;
            }
            if (self.vars.corners.inner > self.vars.corners.outer) {
                self.vars.corners.inner = self.vars.corners.outer;
            }

            self.vars.drawFlags.background = true;
        }
    }

    /**
     * Returns the value of a computed style property.
     *
     * @param {SVGElement} element The SVG element to get a property for.
     * @param {string} property The property to look for
     * @returns {number} value of the property in pixel.
     */
    function getStylePropertyValue(element, property) {
        const styles = getComputedStyle(element);
        const propValue = styles.getPropertyValue(property);

        if (!propValue) {
            return 0;
        }

        const propInfo = /^\s?(\d+(?:.\d+)?)\s*(px|em|rem|%)?\s?$/.exec(propValue);
        if (!propInfo) {
            logger.error("Failed to parse property string", propValue);
            return 0;
        }
        const [, propStr, propUnit] = propInfo;

        switch (propUnit) {
        case "px":
        case undefined:
            return parseFloat(propStr);
        default:
            logger.error("Encountered unsupported unit", propUnit);
            return 0;
        }
    }

    /**
     * Checks whether the controls main element dimensions have changed.
     *
     * @param {*} self Reference to the control.
     * @returns {boolean} `true` if a change was detected, `false` else.
     */
    function haveDisplayPropertiesChanged(self) {
        if (!self.vars.cache) {
            return true;
        } else if (!self.vars.cache.display) {
            return true;
        }

        return false;
    }

    /**
     * Converts degrees to radiants.
     *
     * @param {number} deg Angle value in degrees.
     * @returns {number} Angle value in radiants.
     */
    function degToRad(deg) {
        return Math.PI * (deg / 180);
    }

    /**
     * Returns min if value is lower than min, max if the value is larger than
     * max, or value otherwise.
     *
     * @param {number} min Lower bound of the interval
     * @param {number} value Value to clamp.
     * @param {number} max Upper bound of the interval
     * @returns {number} min if value is lower than min, max if the value is
     *  larger than max, or value otherwise.
     */
    function clamp(min, value, max) {
        return Math.max(min, Math.min(value, max));
    }

    /**
     * Returns coordinates for a value on a rotated line. The line is
     * calculated to span the entire available area from (0,0) to
     * (width,height).
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the area to draw the line in.
     * @param {number} height Height of the area to draw the line in.
     * @param {number} theta Rotation of the bar.
     * @param {number} value Value to return the coordinate for.
     * @param {number} orthogonalOffset Distance orthogonal to the bar to
     *  offset the point by.
     * @returns {{ x: number, y: number }}
     */
    function getPointAtValue(self, width, height, theta, value, orthogonalOffset = 0) {
        const invertBar = !!self.config["fill-inverse"],
            /** @type {{ valueSettings: { min: number, max: number }}} */
            { valueSettings: { min, max } } = self.vars;

        // Cap theta to [0, 2π) and rotate it by 90° since our bar should be
        // horizontal at 0° rotation.
        theta = (2.5 * Math.PI + (theta % (2 * Math.PI))) % (2 * Math.PI);

        // We need the sin/cos values multiple times here so compute them only
        // once.
        const cos = Math.cos(theta),
            sin = Math.sin(theta);

        // Variables for the X and Y coordinates from the center of the
        // viewbox.
        let x, y;

        const r = Math.sqrt((height / 2) ** 2 + (width / 2) ** 2),
            circleX = sin * r,
            circleY = cos * r,
            // Check whether we need to find X or Y.
            coordinateToFind = (-width / 2 <= circleX && circleX <= width / 2) ? 'x' : 'y',
            quadrant = Math.floor(2 * theta / Math.PI),
            // Get the tangens for theta in its current quadrant.
            tanQuad = Math.tan(theta - quadrant * Math.PI / 2);

        if (coordinateToFind === 'x') {
            // We need to find X. Y is known.
            y = Math.sign(circleY) * Math.abs(height) / 2;
            // Depending on the quadrant we need to either multiply by the
            // tangens (even quadrants) or divide by -tangens (uneven
            // quadrants).
            x = (quadrant & 1) === 0 ? y * tanQuad : -y / tanQuad;
        } else if (coordinateToFind === 'y') {
            // We need to find Y. X is known.
            x = Math.sign(circleX) * Math.abs(width) / 2;
            // Depending on the quadrant we need to either divide by the
            // tangens (even quadrants) or multiply by -tangens (uneven
            // quadrants).
            y = (quadrant & 1) === 0 ? x / tanQuad : -x * tanQuad;
        }

        // Scale factor ranges from [-1, 1] where 0 is the center of the bar.
        const scaleFactor = (2 * (value - min) / (max - min) - 1) * (invertBar ? -1 : 1);

        if (x === Infinity || y === Infinity || isNaN(x) || isNaN(y)) {
            return {
                x: null,
                y: null
            };
        }
        return {
            x: width / 2 + x * scaleFactor - cos * orthogonalOffset,
            y: height / 2 - y * scaleFactor - sin * orthogonalOffset
        };
    }

    /**
     * Returns coordinates for a value on a given arc.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     * @param {number} value Value to return the coordinate for.
     * @param {number} radiusOffset
     * @returns {{ x: number, y: number }}
     */
    function getPointAtValueArc(self, arcStart, arcEnd, gaugeBoundingBox, value, radiusOffset = 0) {
        const radius = gaugeBoundingBox.radius + radiusOffset,
            theta = getAngleForValue(self, arcStart, arcEnd, value);

        return {
            x: gaugeBoundingBox.centerX + Math.cos(theta) * radius,
            y: gaugeBoundingBox.centerY + Math.sin(theta) * radius
        };
    }

    /**
     * Returns an SVG path instruction string for the line.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the area to draw the line in.
     * @param {number} height Height of the area to draw the line in.
     * @param {number} rotation Rotation of the area to draw the line in.
     * @param {number} valueStart Value on the gauge to start the line at.
     * @param {number} valueEnd Value on the gauge to end the line at.
     * @param {number} orthogonalOffset Distance orthogonal to the bar to
     *  offset the point by.
     * @param {number} valueOffset Distance to offset the value
     * @returns {string} SVG path instruction string for the arc.
     */
    function renderValueRangeLine(self, width, height, rotation, valueStart, valueEnd, orthogonalOffset = 0, valueOffset = 0) {
        const invertBar = !!self.config["fill-inverse"];

        // Compute the two endpoints of the line.
        const { x: x1, y: y1 } = getPointAtValue(self, width, height, rotation, invertBar ? valueEnd : valueStart, orthogonalOffset),
            { x: x2, y: y2 } = getPointAtValue(self, width, height, rotation, invertBar ? valueStart : valueEnd, orthogonalOffset);

        if (x1 === null || x2 === null || y1 === null || y2 === null) {
            return "";
        }

        const rOffset = getRotatedOffset(rotation + (0.5 * Math.PI), valueOffset);

        if (rotation >= Math.PI && rotation < 2 * Math.PI) {
            if ((x1 - Math.abs(rOffset.x) <= x2 + Math.abs(rOffset.x)) && (y1 - Math.abs(rOffset.y) <= y2 + Math.abs(rOffset.y))) {
                return "";
            } else {
                return [
                    `M ${x1 + rOffset.x} ${y1 + rOffset.y}`,
                    `L ${x2 - rOffset.x} ${y2 - rOffset.y}`
                ].join(" ");
            }
        } else if ((x1 + Math.abs(rOffset.x) >= x2 - Math.abs(rOffset.x)) && (y1 + Math.abs(rOffset.y) >= y2 - Math.abs(rOffset.y))) {
            return "";
        } else {
            return [
                `M ${x1 + rOffset.x} ${y1 + rOffset.y}`,
                `L ${x2 - rOffset.x} ${y2 - rOffset.y}`
            ].join(" ");
        }
    }

    /**
     * Helps to call renderArc() regarding the current rotation of the arc
     *
     * @param {*} self Reference to the control.
     * @param {number} startAngle Starting angle of the arc
     * @param {number} endAngle Ending angle of the arc
     * @param {BoundingBox} boundingBox
     * @param {number} posX xOffset
     * @param {number} posY yOffset
     * @param {number} rotation Rotation
     * @returns {string} SVG path instruction string for the arc.
     */
    function renderRotatedArc(self, startAngle, endAngle, boundingBox, posX, posY, rotation) {
        const { valueSettings } = self.vars;
        return renderArc(self, startAngle + rotation, endAngle + rotation, boundingBox, valueSettings.min, valueSettings.max, posX, posY);
    }

    /**
     * Returns the offset for an object considering its current rotation
     *
     * @param {number} rotation Rotation
     * @param {number} offset Offset to return the coordinate for.
     * @returns {{ x: number, y: number }}
     */
    function getRotatedOffset(rotation, offset) {
        return {
            x: Math.sin(rotation) * offset,
            y: -(Math.cos(rotation) * offset)
        };
    }

    /**
     * Returns an SVG path instruction string for the line.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the area to draw the line in.
     * @param {number} height Height of the area to draw the line in.
     * @param {number} valueStart Value on the gauge to start the line at.
     * @param {number} valueEnd Value on the gauge to end the line at.
     * @param {number} orthogonalOffset Distance orthogonal to the bar to
     *  offset the point by.
     * @returns {string} SVG path instruction string for the arc.
     */
    function renderRoundedBox(self, width, height, rotation, valueStart, valueEnd, r = 0, r2 = 0, orthogonalOffset = 0) {
        const invertBar = !!self.config["fill-inverse"];
        const barWidth = self.vars.cache.gaugeWidth / 2,
            { computeBoundingBoxForArc } = shmi.requires("iqGauge.gaugeHelpers");

        // Compute the four endpoints of the imaginary lines without the offset for the arcs.
        const { x: x1, y: y1 } = getPointAtValue(self, width, height, rotation, invertBar ? valueEnd : valueStart, orthogonalOffset - barWidth - TICK_LENGTH),
            { x: x2, y: y2 } = getPointAtValue(self, width, height, rotation, invertBar ? valueEnd : valueStart, orthogonalOffset + barWidth - TICK_LENGTH),
            { x: x3, y: y3 } = getPointAtValue(self, width, height, rotation, invertBar ? valueStart : valueEnd, orthogonalOffset + barWidth - TICK_LENGTH),
            { x: x4, y: y4 } = getPointAtValue(self, width, height, rotation, invertBar ? valueStart : valueEnd, orthogonalOffset - barWidth - TICK_LENGTH);

        // create a bounding box for both arc radii
        const arcBoundingBox = computeBoundingBoxForArc(r, r, 0, 0.5 * Math.PI),
            arcBoundingBox2 = computeBoundingBoxForArc(r2, r2, 0, 0.5 * Math.PI);

        // calculate the offset for both arcs
        const movR = getRotatedOffset(rotation, r);
        const movR2 = getRotatedOffset(rotation, r2);

        if (x1 === null || x2 === null || x3 === null || x4 === null || y1 === null || y2 === null || y3 === null || y4 === null) {
            return [""];
        }
        return [
            `M ${x1 + movR.x}  ${y1 + movR.y} `,
            `L ${x2 - movR.x}  ${y2 - movR.y} `,
            renderRotatedArc(self, -Math.PI, -0.5 * Math.PI, arcBoundingBox, x2 - movR.y - movR.x, y2 - movR.y + movR.x, rotation),
            `L ${x3 + movR2.y}  ${y3 - movR2.x} `,
            renderRotatedArc(self, -0.5 * Math.PI, 0, arcBoundingBox2, x3 - movR2.x + movR2.y, y3 - movR2.x - movR2.y, rotation),
            `L ${x4 + movR2.x}  ${y4 + movR2.y} `,
            renderRotatedArc(self, 0, 0.5 * Math.PI, arcBoundingBox2, x4 + movR2.x + movR2.y, y4 - movR2.x + movR2.y, rotation),
            `L ${x1 - movR.y}  ${y1 + movR.x} `,
            renderRotatedArc(self, 0.5 * Math.PI, Math.PI, arcBoundingBox, x1 + movR.x - movR.y, y1 + movR.x + movR.y, rotation),
            `z`
        ].join(" ");
    }

    /**
     * Returns the angle from the origin of the arc for the given value. The
     * value is clamped to the gauges limits.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {number} value Value to return the angle for.
     * @returns {number}
     */
    function getAngleForValue(self, arcStart, arcEnd, value) {
        const { valueSettings } = self.vars;

        // Make sure the value stays inside our bounds.
        value = clamp(valueSettings.min, value, valueSettings.max);

        if (self.config["flip-arc-values"]) {
            return arcEnd - (arcEnd - arcStart) / (valueSettings.max - valueSettings.min) * (value - valueSettings.min);
        } else {
            return arcStart + (arcEnd - arcStart) / (valueSettings.max - valueSettings.min) * (value - valueSettings.min);
        }
    }

    /**
     * Returns an SVG path instruction string for the arc.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {BoundingBox} gaugeBoundingBox
     * @param {number} valueStart
     * @param {number} valueEnd
     * @param {number} xOffset
     * @param {number} yOffset
     * @param {number} [radiusOffset]
     * @param {boolean} [invertLong] Inverts the "long" flag of the emitted arc
     *  SVG instruction.
     * @returns {string} SVG path instruction string for the arc.
     */
    function renderArc(self, arcStart, arcEnd, gaugeBoundingBox, valueStart, valueEnd, xOffset = 0, yOffset = 0, radiusOffset = 0, invertLong = false) {
        const flipArcValues = !!self.config["flip-arc-values"],
            radius = gaugeBoundingBox.radius + radiusOffset,
            theta1 = getAngleForValue(self, arcStart, arcEnd, flipArcValues ? valueEnd : valueStart),
            theta2 = getAngleForValue(self, arcStart, arcEnd, flipArcValues ? valueStart : valueEnd),
            // Check if the radius is > 180deg. If so, we need to set the long
            // flag for the SVG.
            long = Math.abs(theta2 - theta1) > Math.PI;

        // Compute the two endpoints of the line.
        const { x: x, y: y } = getPointAtValueArc(self, arcStart, arcEnd, gaugeBoundingBox, flipArcValues ? valueStart : valueEnd, radiusOffset);

        return [
            `A ${radius} ${radius} 0 ${(long !== invertLong) ? 1 : 0} 1 ${x + xOffset} ${y + yOffset}`
        ].join(" ");
    }

    /**
     * Initializes the colored value range lines of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {Set} ticks Set of values where values for ticks are to be
     *  inserted.
     */
    function initGaugeValueRanges(self, ticks) {
        const {
            elements: { gaugePathValueRangeNominal, gaugePathValueRangePrewarn, gaugePathValueRangeWarn },
            valueSettings
        } = self.vars;
        const clampValue = (value) => clamp(valueSettings.min, value, valueSettings.max);

        const newRanges = [
            // Nominal range (green)
            {
                element: gaugePathValueRangeNominal,
                ranges: [
                    {
                        from: clampValue(valueSettings.prewarn.min ?? valueSettings.warn.min ?? valueSettings.min),
                        to: clampValue(valueSettings.prewarn.max ?? valueSettings.warn.max ?? valueSettings.max)
                    }
                ]
            },
            // Prewarn range (yellow)
            {
                element: gaugePathValueRangePrewarn,
                ranges: [
                    {
                        from: clampValue(valueSettings.warn.min ?? valueSettings.min),
                        to: clampValue(valueSettings.prewarn.min ?? valueSettings.warn.min ?? valueSettings.min)
                    },
                    {
                        from: clampValue(valueSettings.prewarn.max ?? valueSettings.warn.max ?? valueSettings.max),
                        to: clampValue(valueSettings.warn.max ?? valueSettings.max)
                    }
                ]
            },
            // Warn range (red)
            {
                element: gaugePathValueRangeWarn,
                ranges: [
                    {
                        from: valueSettings.min,
                        to: clampValue(valueSettings.warn.min ?? valueSettings.min)
                    },
                    {
                        from: clampValue(valueSettings.warn.max ?? valueSettings.max),
                        to: valueSettings.max
                    }
                ]
            }
        ];
        newRanges.forEach(({ element, ranges }) => {
            // Check if there are enough SVG elements for us. This check fails
            // if the template is faulty.
            if (element.children.length !== ranges.length) {
                return;
            }

            ranges.forEach(({ from, to }, idx) => {
                ticks.add(from);
                ticks.add(to);
            });
        });
        return newRanges;
    }
    /**
     * Updates the colored value range lines of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     * @param {Set} ticks Set of values where values for ticks are to be
     *  inserted.
     */
    function positionGaugeValueRanges(self, width, height, rotation, newRanges) {
        const {
            cache: { gaugeWidth },
            valueSettings
        } = self.vars;

        newRanges.forEach(({ element, ranges }) => {
            ranges.forEach(({ from, to }, idx) => {
                if (from >= to || (valueSettings.prewarn.min === null && valueSettings.prewarn.max === null && valueSettings.warn.min === null && valueSettings.warn.max === null)) {
                    // The range has zero length. Hide the element.
                    element.children[idx].removeAttribute("d");
                } else {
                    // Render the line.
                    element.children[idx].setAttribute("d", renderValueRangeLine(self, width, height, rotation, from, to, gaugeWidth / 2, TICK_GAP));
                }
            });
        });
    }
    /**
     * Updates the act value and its bar on the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     */
    function drawGaugeActValue(self, width, height, rotation) {
        const {
            elements: { gaugeMaskPath },
            animators: { actValue: { value: actValueValue } },
            valueSettings
        } = self.vars;
        const invertValue = !!self.config["fill-reverse"];
        const ANIMATION_TOLERANCE = .25;

        // Define a few helper binds so we don't have to throw so many arguments around all the time.

        /** @type {(value: number, orthogonalOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, width, height, rotation),
            /** @type {(valueStart: number, valueEnd: number, orthogonalOffset?: number) => string} */
            clampValue = (value) => clamp(valueSettings.min, value, valueSettings.max);

        const value = clampValue(actValueValue);
        const { x: originX, y: originY } = getPointAtValueHelper(0, 0);
        const { x: maxX, y: maxY } = getPointAtValueHelper(valueSettings.max, 0);
        const { x: minX, y: minY } = getPointAtValueHelper(valueSettings.min, 0);
        const { x: invX, y: invY } = getPointAtValueHelper(valueSettings.max - value, 0);
        const movX = invertValue ? originX - invX - minX + maxX : originX - invX;
        const movY = invertValue ? originY - invY - minY + maxY : originY - invY;
        if (Math.abs(movX - (self.vars.cache.gaugeMaskPathX ?? 0)) > ANIMATION_TOLERANCE || Math.abs(movY - (self.vars.cache.gaugeMaskPathY ?? 0)) > ANIMATION_TOLERANCE) {
            gaugeMaskPath.style.transform = `translateX(${movX}px) translateY(${movY}px)`;
            self.vars.cache.gaugeMaskPathX = movX;
            self.vars.cache.gaugeMaskPathY = movY;
        }
    }

    /**
     * Updates the tolerance limit lines on the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     */
    function drawGaugeTolerances(self, width, height, rotation) {
        const {
            cache: { gaugeWidth },
            elements: { gaugeLineToleranceMin, gaugeLineToleranceMax },
            valueSettings: { min, max },
            valueSettings
        } = self.vars;

        /** @type {(value: number, orthogonalOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, width, height, rotation);

        if (valueSettings.tolMin === null || valueSettings.tolMin < min) {
            // No min value or value outside of display limits. Hide the limit
            // line.
            gaugeLineToleranceMin.removeAttribute("x1");
            gaugeLineToleranceMin.removeAttribute("y1");
            gaugeLineToleranceMin.removeAttribute("x2");
            gaugeLineToleranceMin.removeAttribute("y2");
        } else {
            // Compute the two points for our set value limit line. The line
            // spans the entire gauge width and extends a bit further
            // "inwards". The hidden marker is positioned where the set value
            // marker would be, if it was at the lower bound.
            const pStart = getPointAtValueHelper(valueSettings.tolMin, -gaugeWidth / 2 - TICK_LABEL_DISTANCE),
                pEnd = getPointAtValueHelper(valueSettings.tolMin, gaugeWidth / 2);

            // Limit line.
            gaugeLineToleranceMin.setAttribute("x1", pStart.x ? pStart.x : 0);
            gaugeLineToleranceMin.setAttribute("y1", pStart.y ? pStart.y : 0);
            gaugeLineToleranceMin.setAttribute("x2", pEnd.x ? pEnd.x : 0);
            gaugeLineToleranceMin.setAttribute("y2", pEnd.y ? pEnd.y : 0);
        }

        if (valueSettings.tolMax === null || valueSettings.tolMax > max) {
            // No max value or value outside of display limits. Hide the limit
            // line.
            gaugeLineToleranceMax.removeAttribute("x1");
            gaugeLineToleranceMax.removeAttribute("y1");
            gaugeLineToleranceMax.removeAttribute("x2");
            gaugeLineToleranceMax.removeAttribute("y2");
        } else {
            // Compute the two points for our set value limit line. The line
            // spans the entire gauge width and extends a bit further
            // "inwards". The hidden marker is positioned where the set value
            // marker would be, if it was at the upper bound.
            const pStart = getPointAtValueHelper(valueSettings.tolMax, -gaugeWidth / 2 - TICK_LABEL_DISTANCE),
                pEnd = getPointAtValueHelper(valueSettings.tolMax, gaugeWidth / 2);

            // Limit line.
            gaugeLineToleranceMax.setAttribute("x1", pStart.x ? pStart.x : 0);
            gaugeLineToleranceMax.setAttribute("y1", pStart.y ? pStart.y : 0);
            gaugeLineToleranceMax.setAttribute("x2", pEnd.x ? pEnd.x : 0);
            gaugeLineToleranceMax.setAttribute("y2", pEnd.y ? pEnd.y : 0);
        }
    }

    /**
     * Updates the value tick group of the SVG. SVG elements may be generated
     * in the group, if it does not have enough children for all ticks to be
     * displayed.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     * @param {Set} ticks Set of values where ticks are to be displayed at.
     */
    function drawGaugeValueRangeTicks(self, width, height, rotation, ticks) {
        const {
            cache: { gaugeWidth },
            elements: { gaugePathValueRangeTicks },
            valueSettings
        } = self.vars;

        /** @type {(value: number, orthogonalOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, width, height, rotation);

        let currentTick = 0;
        ticks.forEach((value) => {
            if (valueSettings.min > value || value > valueSettings.max) {
                // Don't create graphical markers for values outside of the bar.
                return;
            }

            // Compute the starting and ending point of the current tick. The
            // tick is to overlap the value range bars, so we need to account
            // for that.
            const pStart = getPointAtValueHelper(value, gaugeWidth / 2 - 1),
                pEnd = getPointAtValueHelper(value, gaugeWidth / 2 + TICK_LENGTH);

            // If there's not enough children in the group, generate more.
            while (gaugePathValueRangeTicks.children.length <= currentTick) {
                gaugePathValueRangeTicks.append(document.createElementNS("http://www.w3.org/2000/svg", "line"));
            } // while ( ... )

            /** @type {SVGElement} */
            const lineElement = gaugePathValueRangeTicks.children[currentTick];
            lineElement.setAttribute("x1", pStart.x ? pStart.x : 0);
            lineElement.setAttribute("y1", pStart.y ? pStart.y : 0);
            lineElement.setAttribute("x2", pEnd.x ? pEnd.x : 0);
            lineElement.setAttribute("y2", pEnd.y ? pEnd.y : 0);

            ++currentTick;
        });

        // Hide elements not needed right now. We don't outright remove them
        // because we might need them later on and can do so faster if they
        // already exist.
        for (let i = currentTick; i < gaugePathValueRangeTicks.children.length; ++i) {
            /** @type {SVGElement} */
            const lineElement = gaugePathValueRangeTicks.children[i];

            lineElement.removeAttribute("x1");
            lineElement.removeAttribute("y1");
            lineElement.removeAttribute("x2");
            lineElement.removeAttribute("y2");
        } // for ( ... )
    }

    /**
     * Draws the value labels of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {Set} ticks Set of values where ticks are to be displayed at.
     * @param {Set} tolTicks Set of values where tolerance ticks are to be displayed at.
     */
    function drawGaugeValueRangeLabels(self, ticks, tolTicks) {
        const {
            elements: { gaugePathValueRangeLabels, gaugePathTolRangeLabels },
            valueSettings
        } = self.vars;

        /** @type {{left: number; right: number; top: number; bottom: number; value: number; element: SVGGraphicsElement}[]} */
        let valueBoxes = [],
            tolBoxes = [];

        let currentTick = 0;
        const largestBox = {
            width: 0,
            height: 0
        };

        function addValueBoxes(targetLabels, targetBoxes, value) {
            // Don't create labels for ticks outside the display range.
            if (valueSettings.min > value || value > valueSettings.max) {
                return;
            }
            // If there's not enough children in the group, generate more.
            while (targetLabels.children.length <= currentTick) {
                targetLabels.append(document.createElementNS("http://www.w3.org/2000/svg", "text"));
            } // while ( ... )

            /** @type {SVGGraphicsElement} */
            const textElement = targetLabels.children[currentTick];

            if (valueSettings["decimal-delimiter"]) {
                textElement.textContent = valueSettings.actValuePrecision >= 0 ? value.toFixed(valueSettings.actValuePrecision).replace(".", valueSettings["decimal-delimiter"]) : value.toString();
            } else {
                textElement.textContent = valueSettings.actValuePrecision >= 0 ? value.toFixed(valueSettings.actValuePrecision) : value.toString();
            }

            const textBB = textElement.getBBox();
            targetBoxes.push({
                element: textElement,
                left: null,
                top: null,
                right: null,
                bottom: null,
                width: textBB.width,
                height: textBB.height,
                value
            });
            if (textBB.width > largestBox.width) {
                largestBox.width = textBB.width;
            }
            if (textBB.height > largestBox.height) {
                largestBox.height = textBB.height;
            }
            ++currentTick;
        }
        tolTicks.forEach((value) => {
            addValueBoxes(gaugePathTolRangeLabels, tolBoxes, value);
        });
        currentTick = 0;
        ticks.forEach((value) => {
            addValueBoxes(gaugePathValueRangeLabels, valueBoxes, value);
        });

        // Remove elements not needed right now. We don't outright remove them
        // because we might need them later on and can do so faster if they
        // already exist.
        for (let i = currentTick; i < gaugePathValueRangeLabels.children.length; ++i) {
            /** @type {SVGElement} */
            const textElement = gaugePathValueRangeLabels.children[i];
            textElement.textContent = "";
            textElement.removeAttribute("x");
            textElement.removeAttribute("y");
        } // for ( ... )

        return {
            largestBox,
            valueBoxes,
            tolBoxes
        };
    }

    /**
     * Positions a value or tolerance label box.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     * @param {valueBox} valueBox value box to position
     * @param {boolean} isTolBox is tolerance or value box
     */
    function doPositioning(self, width, height, rotation, valueBox, isTolBox) {
        const { getAnchorCoordinateForRect } = shmi.requires("iqGauge.gaugeHelpers"),
            {
                cache: { gaugeWidth }
            } = self.vars;

        /** @type {(value: number, orthogonalOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, width, height, rotation);

        // Point to anchor the label at.
        const coords = getPointAtValueHelper(valueBox.value, isTolBox ? -(gaugeWidth / 2 + TICK_LENGTH + TICK_LABEL_DISTANCE) : gaugeWidth / 2 + TICK_LENGTH + TICK_LABEL_DISTANCE),
            // Value labels are positioned orthogonal to the gauge (in
            // line"ish" with the ticks themselves). We need to compute the
            // anchor point on the text itself.
            //
            // Treat the text's bounding box as an ellipsis and anchor the text
            // on the side facing the bar.
            offsetMult = {
                cos: Math.cos(rotation - Math.PI / 2),
                sin: Math.sin(rotation - Math.PI / 2)
            };
        // Compute the anchor. The text alignment is such that the origin
        // is in the center of the bounding box.
        const { x: offsetX, y: offsetY } = getAnchorCoordinateForRect(valueBox.width, valueBox.height, offsetMult.sin, offsetMult.cos);

        if (isNaN(coords.x) || isNaN(coords.y) || isNaN(offsetX) || isNaN(offsetY)) { // will be hidden later
            return;
        }
        valueBox.left = isTolBox ? coords.x - offsetX - valueBox.width / 2 : coords.x + offsetX - valueBox.width / 2;
        valueBox.top = isTolBox ? coords.y - offsetY - valueBox.height / 2 : coords.y + offsetY - valueBox.height / 2;
        valueBox.right = isTolBox ? coords.x + valueBox.width / 2 - offsetX : coords.x + valueBox.width / 2 + offsetX;
        valueBox.bottom = isTolBox ? coords.y + valueBox.height / 2 - offsetY : coords.y + valueBox.height / 2 + offsetY;

        valueBox.element.setAttribute("x", isTolBox ? coords.x - offsetX : coords.x + offsetX);
        valueBox.element.setAttribute("y", isTolBox ? coords.y - offsetY : coords.y + offsetY);
    }

    /**
     * Positions the value labels of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     * @param {Set} valueBoxes Set of value boxes
     * @param {Set} tolBoxes Set of tolerance boxes
     */
    function positionGaugeValueRangeLabels(self, width, height, rotation, valueBoxes, tolBoxes) {
        valueBoxes.forEach((box) => {
            doPositioning(self, width, height, rotation, box, false);
        });
        tolBoxes.forEach((box) => {
            doPositioning(self, width, height, rotation, box, true);
        });
    }

    /**
     * Fix collision of value boxes too close to another
     *
     * @param {Set} valueBoxes Set of value boxes
     * @param {valueBox} outerBox value box to check collision for
     * @param {number} outerIdx index of the value box
     */
    function fixCollision(valueBoxes, outerBox, outerIdx) {
        // Remove labels that collide. Prioritize the outermost labels.
        const boxText = (outer, inner) => (
            (
                (outer.left <= inner.left && inner.left <= outer.right) ||
                (outer.left <= inner.right && inner.right <= outer.right)
            ) && (
                (outer.top <= inner.top && inner.top <= outer.bottom) ||
                (outer.top <= inner.bottom && inner.bottom <= outer.bottom)
            )
        );

        if (outerBox.element.textContent === "") {
            // Element has already been hidden and can't collide anymore.
            return;
        }

        // Find collisions with the current element.
        const collisions = valueBoxes.filter((innerBox, innerIdx) => innerIdx > outerIdx &&
            innerBox.element.textContent !== "" && (boxText(outerBox, innerBox) || boxText(innerBox, outerBox))
        );

        // Remove colliding elements.
        collisions.forEach((collidedBox) => {
            collidedBox.element.textContent = "";
            collidedBox.element.removeAttribute("x");
            collidedBox.element.removeAttribute("y");
        });
    }

    /**
     * Sorts and fixes all collisions of value and tolerance boxes
     *
     * @param {Set} valueBoxes Set of value boxes
     * @param {Set} tolBoxes Set of tolerance boxes
     */
    function sortAndFixGaugeValueRangeLabels(valueBoxes, tolBoxes) {
        // Sort the values inversely by their absolute distance from the
        // center value. By removing the first collision we find going by that
        // order, we preserve the outermost values.
        valueBoxes.sort(({ value: lhs }, { value: rhs }) => {
            const avg = (valueBoxes[0].value + valueBoxes[1].value) / 2;

            return Math.abs(rhs - avg) - Math.abs(lhs - avg);
        });

        tolBoxes.sort(({ value: lhs }, { value: rhs }) => {
            const avg = (tolBoxes[0].value + tolBoxes[1].value) / 2;

            return Math.abs(rhs - avg) - Math.abs(lhs - avg);
        });

        valueBoxes.forEach((outerBox, outerIdx) => {
            fixCollision(valueBoxes, outerBox, outerIdx);
        });

        tolBoxes.forEach((outerBox, outerIdx) => {
            fixCollision(tolBoxes, outerBox, outerIdx);
        });
    }

    /**
     * Updates the components of the SVG that have been flagged.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} rotation Rotation of the gauge in radiants.
     */
    function drawGaugeSvg(self, width, height, rotation) {
        const {
            cache: { gaugeWidth },
            corners: { inner, outer },
            elements: { gaugePathFill, gaugePathActualValueMin, gaugePathActualValueMax, gaugeContentContainer, gaugeMaskPath, gaugePathActualValueBar },
            actValue: { value: actValue },
            valueSettings,
            drawFlags
        } = self.vars;

        // Check if there's something for us to update.
        if (Object.values(drawFlags).every((flag) => !flag)) {
            // Nothing to update.
            return;
        }

        const valueTicks = new Set,
            tolTicks = new Set;
        let valueRanges = null,
            boxInfo = null;

        if (drawFlags.background) {
            // Updates to the background may cause other things to change as
            // well. Just redraw everything.
            drawFlags.actValue = true;
            drawFlags.actLimits = true;
        }

        if (drawFlags.actLimits || drawFlags.tolLimits) {
            valueTicks.add(valueSettings.min);
            valueTicks.add(valueSettings.max);

            // Add set item limits (if set) to list of ticks.
            if (valueSettings.tolMin !== null) {
                tolTicks.add(valueSettings.tolMin);
            }

            if (valueSettings.tolMax !== null) {
                tolTicks.add(valueSettings.tolMax);
            }

            valueRanges = initGaugeValueRanges(self, valueTicks);

            if (self.config["value-labels"] > 1) {
                // Render labels for value range ticks.
                boxInfo = drawGaugeValueRangeLabels(self, valueTicks, tolTicks);
            }
        }

        // The gauge width only matters for everything that is orthogonal to
        // the gauge (the SVG path ends exactly at the given coordinates).
        // Compute portion of the gauge width for horizontal and vertical
        // computations.
        const hFac = Math.sin(rotation),
            vFac = Math.cos(rotation);
        const vGaugeWidth = (gaugeWidth ?? 0) * Math.abs(vFac),
            hGaugeWidth = (gaugeWidth ?? 0) * Math.abs(hFac),
            tickOffsetH = 2 * (TICK_LABEL_DISTANCE + TICK_LENGTH) * Math.abs(hFac) + 2 * TICK_WIDTH * Math.abs(vFac),
            tickOffsetV = 2 * (TICK_LABEL_DISTANCE + TICK_LENGTH) * Math.abs(vFac) + 2 * TICK_WIDTH * Math.abs(hFac) + TICK_LENGTH * Math.abs(vFac);

        if (boxInfo && boxInfo.largestBox.width !== self.vars.cache.boxWidth) { // text boundaries have changed, redraw background
            drawFlags.background = true;
        }
        self.vars.cache.boxWidth = boxInfo?.largestBox.width ?? self.vars.cache.boxWidth ?? 0;
        self.vars.cache.boxHeight = boxInfo?.largestBox.height ?? self.vars.cache.boxHeight ?? 0;

        const labelOffsetX = Math.abs(clamp(-self.vars.cache.boxWidth, isNaN(self.vars.cache.boxWidth * hFac / Math.abs(vFac)) ? 0 : self.vars.cache.boxWidth * hFac / Math.abs(vFac), self.vars.cache.boxWidth)),
            labelOffsetY = Math.abs(clamp(-self.vars.cache.boxHeight, isNaN(self.vars.cache.boxHeight * vFac / Math.abs(hFac)) ? 0 : self.vars.cache.boxHeight * vFac / Math.abs(hFac), self.vars.cache.boxHeight));

        const tempWidth = width - self.vars.cache.boxWidth - labelOffsetX - hGaugeWidth - (self.config["value-labels"] ? tickOffsetH : 0),
            tempHeight = height - self.vars.cache.boxHeight - labelOffsetY - vGaugeWidth - (self.config["value-labels"] ? tickOffsetV : 0);

        const computedWidth = clamp(1, tempWidth, Number.POSITIVE_INFINITY);
        const computedHeight = clamp(1, tempHeight, Number.POSITIVE_INFINITY);

        if (drawFlags.actLimits || drawFlags.tolLimits || drawFlags.background) {
            const translateX = hGaugeWidth / 2 + self.vars.cache.boxWidth / 2 + labelOffsetX / 2 + (self.config["value-labels"] ? tickOffsetH / 2 : 0) + TICK_LENGTH * Math.sin(rotation),
                translateY = vGaugeWidth / 2 + self.vars.cache.boxHeight / 2 + labelOffsetY / 2 + (self.config["value-labels"] ? tickOffsetV / 2 : 0) - TICK_LENGTH * Math.cos(rotation);

            gaugeContentContainer.setAttribute("transform", `translate(${translateX} ${translateY})`);
        }

        // Define a few helper binds so we don't have to throw so many arguments around all the time.

        /** @type {(value: number, orthogonalOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, computedWidth, computedHeight, rotation),
            /** @type {(valueStart: number, valueEnd: number, orthogonalOffset?: number) => string} */
            renderRoundedBoxHelper = renderRoundedBox.bind(null, self, computedWidth, computedHeight, rotation),
            clampValue = (value) => clamp(valueSettings.min, value, valueSettings.max);

        // When to update the bounding box?
        // - Updates to the background may change the size and shape of the
        //   gauge.
        // - Updates to act limits may change the value label's contents and
        //   position.
        if (drawFlags.background) {
            const pStartMin = getPointAtValueHelper(clampValue(valueSettings.min), -gaugeWidth / 2),
                pStartMax = getPointAtValueHelper(clampValue(valueSettings.max), -gaugeWidth / 2),
                pEndMin = getPointAtValueHelper(clampValue(valueSettings.min), gaugeWidth / 2),
                pEndMax = getPointAtValueHelper(clampValue(valueSettings.max), gaugeWidth / 2);

            // Render the main background.
            gaugePathFill.setAttribute("d", renderRoundedBoxHelper(valueSettings.min, valueSettings.max, outer, outer, 0));

            gaugePathActualValueMin.setAttribute("x1", pStartMin.x ? pStartMin.x : 0);
            gaugePathActualValueMin.setAttribute("y1", pStartMin.y ? pStartMin.y : 0);
            gaugePathActualValueMin.setAttribute("x2", pEndMin.x ? pEndMin.x : 0);
            gaugePathActualValueMin.setAttribute("y2", pEndMin.y ? pEndMin.y : 0);

            gaugePathActualValueMax.setAttribute("x1", pStartMax.x ? pStartMax.x : 0);
            gaugePathActualValueMax.setAttribute("y1", pStartMax.y ? pStartMax.y : 0);
            gaugePathActualValueMax.setAttribute("x2", pEndMax.x ? pEndMax.x : 0);
            gaugePathActualValueMax.setAttribute("y2", pEndMax.y ? pEndMax.y : 0);

            // bar is always drawn from min to max and clipped with the clipping mask
            gaugePathActualValueBar.setAttribute("d", renderRoundedBoxHelper(valueSettings.min, valueSettings.max, outer, outer, 0));

            // add a mask for the gauge that will be transformed to represent the current act value
            gaugeMaskPath.setAttribute("d", renderRoundedBoxHelper(valueSettings.min, valueSettings.max, inner, inner, 0));

            drawFlags.background = false;
        }

        // Render main part of the gauge.
        if (drawFlags.actValue) {
            drawGaugeActValue(self, computedWidth, computedHeight, rotation);

            // Update warn state classes
            if (!self.config["disable-alarm-styling"] && self.config.item) {
                if (actValue <= (valueSettings.prewarn.min ?? Number.NEGATIVE_INFINITY) || (valueSettings.prewarn.max ?? Number.POSITIVE_INFINITY) <= actValue) {
                    if (!self.element.classList.contains("preWarn")) {
                        self.element.classList.add("preWarn");
                    }
                } else if (self.element.classList.contains("preWarn")) {
                    self.element.classList.remove("preWarn");
                }

                if (actValue <= (valueSettings.warn.min ?? Number.NEGATIVE_INFINITY) || (valueSettings.warn.max ?? Number.POSITIVE_INFINITY) <= actValue) {
                    if (!self.element.classList.contains("warn")) {
                        self.element.classList.add("warn");
                    }
                } else if (self.element.classList.contains("warn")) {
                    self.element.classList.remove("warn");
                }
            }

            if (self.config["value-labels"] > 0) {
                drawGaugeTolerances(self, computedWidth, computedHeight, rotation);
            }
            drawFlags.actValue = false;
        }

        if (drawFlags.actLimits || drawFlags.tolLimits) {
            if (self.config["value-labels"] > 0) {
                positionGaugeValueRanges(self, computedWidth, computedHeight, rotation, valueRanges);
                // Render value range ticks.
                drawGaugeValueRangeTicks(self, computedWidth, computedHeight, rotation, valueTicks);
                if (boxInfo) {
                    positionGaugeValueRangeLabels(self, computedWidth, computedHeight, rotation, boxInfo.valueBoxes, boxInfo.tolBoxes);
                    sortAndFixGaugeValueRangeLabels(boxInfo.valueBoxes, boxInfo.tolBoxes);
                }
            }

            drawFlags.actLimits = false;
            drawFlags.tolLimits = false;
        }
    }

    /**
     * Updates the svg's internal resolution to fit the element's dimensions.
     *
     * @param {*} self Reference to the control.
     */
    function updateSvgResolution(self) {
        const { clientWidth, clientHeight } = self.vars.elements.svg,
            { svg: svgEl } = self.vars.elements;

        if (svgEl.width.baseVal.value !== svgEl.clientWidth) {
            svgEl.setAttribute("width", svgEl.clientWidth);
            self.vars.drawFlags.background = true;
        }

        if (svgEl.height.baseVal.value !== svgEl.clientHeight) {
            svgEl.setAttribute("height", svgEl.clientHeight);
            self.vars.drawFlags.background = true;
        }

        self.vars.cache.display = {
            controlWidth: clientWidth,
            controlHeight: clientHeight
        };
    }

    /**
     * Checks if the control is ready for drawing.
     *
     * @param {*} self Reference to the control.
     * @returns {boolean} `true` if the control is ready, `false` else.
     */
    function isReady(self) {
        return Object.values(self.vars.elements).every((element) => !!element);
    }

    /**
     * Force a redraw of the gauge.
     *
     * @param {*} self Reference to the control.
     */
    function forceRedraw(self) {
        const { drawFlags } = self.vars;

        // Make sure the cache variables are present.
        checkCache(self);

        if (haveDisplayPropertiesChanged(self)) {
            updateSvgResolution(self);

            drawFlags.background = true;
        }

        drawGaugeSvg(self, self.vars.cache.display.controlWidth, self.vars.cache.display.controlHeight, degToRad(self.config["rotation"] + self.vars.addRotationDegrees ?? 0));
    }

    /**
     * Schedules a control update on the next animation frame. If an update has
     * already been scheduled but not processed, will do nothing.
     *
     * @param {*} self Reference to the control.
     * @returns {?number} Id of the animation frame or `null` if an update
     *  has already been scheduled.
     */
    function startDrawing(self) {
        if (self.vars.animationFrameToken) {
            return null;
        }

        return shmi.raf((timestamp) => {
            if (!isReady(self)) {
                return;
            }

            const { drawFlags, elements: { labelValue } } = self.vars;

            // Process act/set value animations. If anything changed, set the
            // respective draw flags.
            drawFlags.actValue = self.vars.animators.actValue.tick(timestamp);

            // Make sure the cache variables are present.
            checkCache(self);

            if (labelValue && drawFlags.actValueLabel && self.config["show-value"]) {
                labelValue.textContent = self.imports.nv.formatOutput(self.vars.actValue.value ?? self.config["default-value"], self);
                drawFlags.actValueLabel = false;
            }

            if (haveDisplayPropertiesChanged(self)) {
                updateSvgResolution(self);

                drawFlags.background = true;
            }

            if (self.vars.elementIsInViewport) {
                drawGaugeSvg(self, self.vars.cache.display.controlWidth, self.vars.cache.display.controlHeight, degToRad(self.config["rotation"] + self.vars.addRotationDegrees ?? 0));
            }
            // Set the token to `null`, as `startDrawing` checks for that.
            self.vars.animationFrameToken = null;
            self.vars.animationFrameToken = startDrawing(self);
        });
    }

    /**
     * Cancel the current animation frame. If no animation frame has been
     * requested, does nothing.
     *
     * @param {*} self Reference to the control.
     */
    function stopDrawing(self) {
        if (self.vars.animationFrameToken) {
            shmi.caf(self.vars.animationFrameToken);
            self.vars.animationFrameToken = null;
        }
    }

    /**
     * Adapts a DOM event listener to the visuals listener interface.
     *
     * @param {Element} domElement Element to attach the event listener to.
     * @param {string} eventName Name of the event to listen for.
     * @param {function} eventHandler Event handler function.
     *
     * @returns {{enable: function, disable: function}}
     */
    function makeDomEventListener(domElement, eventName, eventHandler) {
        return {
            enable: domElement.addEventListener.bind(domElement, eventName, eventHandler),
            disable: domElement.removeEventListener.bind(domElement, eventName, eventHandler)
        };
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            elements: {
                svg: null,
                gaugeContentContainer: null,
                gaugePathFill: null,
                gaugePathActualValueMin: null,
                gaugePathActualValueMax: null,
                gaugePathActualValueBar: null,
                gaugePathValueRangeNominal: null,
                gaugePathValueRangePrewarn: null,
                gaugePathValueRangeWarn: null,
                gaugePathValueRangeTicks: null,
                gaugePathValueRangeLabels: null,
                gaugePathTolRangeLabels: null,
                gaugeLineToleranceMin: null,
                gaugeLineToleranceMax: null,
                gaugeMask: null,
                gaugeMaskPath: null,
                gaugeSvgSizeObserver: null
            },
            elementIsInViewport: true,
            hasChanged: true,
            animationFrameToken: null,
            actValue: {
                value: null
            },
            valueSettings: {
                type: shmi.c("TYPE_FLOAT"),
                actValuePrecision: 1,
                min: 0,
                max: 100,
                prewarn: {
                    min: null,
                    max: null
                },
                warn: {
                    min: null,
                    max: null
                },
                tolMin: null,
                tolMax: null
            },
            cache: {
                gaugeWidth: null,
                boxWidth: 0,
                boxHeight: 0,
                gaugeMaskPathX: 0,
                gaugeMaskPathY: 0
            },
            corners: {
                outer: 12,
                inner: 4
            },
            addRotationDegrees: 0,
            animators: {
                actValue: null
            },
            drawFlags: {
                background: true,
                actLimits: true,
                actValue: true,
                tolLimits: true,
                actValueLabel: true
            },
            listeners: [],
            subscriptions: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            onInit: function onInit() {
                const { AnimationHelper } = shmi.requires("iqGauge.animationHelper"),
                    { applyIcon } = shmi.requires("visuals.tools.icons");
                /***********/
                /*** DOM ***/
                /***********/
                this.vars.elements.svg = shmi.getUiElement("gauge-svg", this.element);

                if (!this.vars.elements.svg) {
                    logger.error("Gauge SVG root not found in template.");
                    return;
                }

                // Get mandatory elements.
                this.vars.elements.gaugePathFill = shmi.getUiElement("gauge-fill", this.vars.elements.svg);
                this.vars.elements.gaugeContentContainer = shmi.getUiElement("content-container", this.vars.elements.svg);
                this.vars.elements.gaugePathActualValueMin = shmi.getUiElement("gauge-actual-value-min", this.vars.elements.svg);
                this.vars.elements.gaugePathActualValueMax = shmi.getUiElement("gauge-actual-value-max", this.vars.elements.svg);
                this.vars.elements.gaugePathActualValueBar = shmi.getUiElement("gauge-actual-value-bar", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeNominal = shmi.getUiElement("gauge-value-range-nominal", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangePrewarn = shmi.getUiElement("gauge-value-range-prewarn", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeWarn = shmi.getUiElement("gauge-value-range-warn", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeTicks = shmi.getUiElement("gauge-value-range-ticks", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeLabels = shmi.getUiElement("gauge-value-range-labels", this.vars.elements.svg);
                this.vars.elements.gaugePathTolRangeLabels = shmi.getUiElement("gauge-tol-range-labels", this.vars.elements.svg);
                this.vars.elements.gaugeLineToleranceMin = shmi.getUiElement("gauge-tolerance-value-min", this.vars.elements.svg);
                this.vars.elements.gaugeLineToleranceMax = shmi.getUiElement("gauge-tolerance-value-max", this.vars.elements.svg);
                this.vars.elements.gaugeMask = shmi.getUiElement("gauge-mask", this.vars.elements.svg);
                this.vars.elements.gaugeMaskPath = shmi.getUiElement("gauge-mask-path", this.vars.elements.svg);
                this.vars.elements.gaugeSvgSizeObserver = shmi.getUiElement("gauge-svg-size-observer", this.element);

                // Check if we're missing an element.
                const missingElements = Object.entries(this.vars.elements).filter(([name, element]) => !element).map(([name]) => name);
                if (missingElements.length > 0) {
                    logger.error(`Some elements are missing. Control unable to initialize:\n  - ${missingElements.join("\n  - ")}`);
                    return;
                }

                // Get optional elements.
                this.vars.elements.icon = shmi.getUiElement("icon", this.element);
                this.vars.elements.label = shmi.getUiElement("label", this.element);
                this.vars.elements.labelValue = shmi.getUiElement("value", this.element);
                this.vars.elements.labelUnit = shmi.getUiElement("unit", this.element);

                // Delete unavailable optional elements so that the ready check doesn't fail.
                Object.entries(this.vars.elements).forEach(([name, element]) => {
                    if (!element) {
                        delete this.vars.elements[name];
                    }
                });

                const gaugeMaskId = `gauge-mask-${getRandomId()}`;
                this.vars.elements.gaugeMask.setAttribute("id", gaugeMaskId);
                this.vars.elements.gaugePathActualValueBar.style.mask = `url(#${gaugeMaskId})`;

                // Resize svg's internal resolution to fit control.
                updateSvgResolution(this);

                // Icon
                if (this.vars.elements.icon && this.config['icon-src']) {
                    applyIcon(this.vars.elements.icon, this.config["icon-src"], null, true);
                    shmi.addClass(this.vars.elements.icon, "inlined");
                } else if (this.vars.elements.icon && this.config['icon-class']) {
                    applyIcon(this.vars.elements.icon, null, this.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(this.element, "no-icon");
                }

                if (this.config["value-labels"] > 0) {
                    shmi.addClass(this.element, "has-ticks");
                }
                if (this.config["value-labels"] > 1) {
                    shmi.addClass(this.element, "has-labels");
                }
                if (!this.config["show-value"]) {
                    shmi.addClass(this.element, "no-value");
                }

                // Initialize label.
                setLabelImpl(this, this.config.label);

                // Initialize unit text.
                setUnitTextImpl(this, this.config['unit-text']);

                this.vars.valueSettings["decimal-delimiter"] = this.config["decimal-delimiter"] ? shmi.localize(this.config["decimal-delimiter"]) : ".";
                this.vars.cache.gaugeWidth = getStylePropertyValue(this.vars.elements.gaugePathFill, "width");
                this.vars.corners.outer = getStylePropertyValue(this.element, "--corner-radius-outer");
                this.vars.corners.inner = getStylePropertyValue(this.element, "--corner-radius-inner");
                this.vars.addRotationDegrees = getStylePropertyValue(this.element, "--add-rotation-degrees");

                if (this.vars.corners.outer > this.vars.cache.gaugeWidth / 2) {
                    this.vars.corners.outer = this.vars.cache.gaugeWidth / 2;
                }
                if (this.vars.corners.inner > this.vars.corners.outer) {
                    this.vars.corners.inner = this.vars.corners.outer;
                }
                this.vars.animators.actValue = new AnimationHelper;

                // Setup the intersection observer. This observer is
                // responsible for pausing redraws while the widget is not in
                // the current viewport.
                this.vars.intersectionObserver = new IntersectionObserver((entries) => {
                    // If there's more than one entry, we're only really
                    // interested in the latest one.
                    if (entries.length > 1) {
                        entries.sort((lhs, rhs) => rhs.time - lhs.time);
                    }
                    this.vars.elementIsInViewport = entries[0].isIntersecting;
                });

                this.vars.resizeObserver = new ResizeObserver(() => {
                    this.vars.cache = null;
                    forceRedraw(this);
                });

                // Should anything change with our element, invalidate the
                // cache. This will cause everything to be recomputed and
                // redrawn on the next frame.
                this.vars.mutationObserver = new MutationObserver(() => {
                    this.vars.cache = null;
                });
            },
            onEnable: function onEnable() {
                if (!isReady(this)) {
                    return;
                }

                // Attach the observers.
                this.vars.intersectionObserver?.observe(this.vars.elements.svg);
                this.vars.mutationObserver?.observe(this.element, { attributes: true, attributeFilter: ['style', 'class'] });
                this.vars.resizeObserver?.observe(this.vars.elements.gaugeSvgSizeObserver);

                // Register resize handler. Workaround for detecting style
                // changes. Clears cache on every `resize` event, causing
                // styles to be reevaluated.
                this.vars.listeners.push(makeDomEventListener(window, "resize", () => {
                    // Clearing the cache causes `haveDisplayPropertiesChanged` to
                    // return `true`, which in turn causes the CSS to be reevaluated.
                    this.vars.cache = null;
                }));

                checkCache(this);
                startDrawing(this);

                // Subscribe act item (if configured).
                if (this.config["item"]) {
                    this.vars.subscriptions.push(this.imports.im.subscribeItem(this.config["item"], {
                        setValue: (value) => {
                            this.vars.actValue.value = value;
                            this.vars.animators.actValue.set(value, parseInt(this.config["animation-duration"]) ?? 0);
                            this.vars.drawFlags.actValueLabel = true;
                        },
                        setProperties: (min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision, tolmin, tolmax) => {
                            // Make sure the values are well behaved.
                            min = (this.config["auto-min"] ? min : null) ?? this.config["min"] ?? 0;
                            max = (this.config["auto-max"] ? max : null) ?? this.config["max"] ?? 100;
                            prewarnmin = prewarnmin ?? null;
                            prewarnmax = prewarnmax ?? null;
                            warnmin = warnmin ?? null;
                            warnmax = warnmax ?? null;
                            tolmin = tolmin ?? null;
                            tolmax = tolmax ?? null;
                            type = (this.config["auto-type"] ? type : null) ?? this.config["type"] ?? shmi.c("TYPE_FLOAT");
                            precision = (this.config["auto-precision"] ? precision : null) ?? this.config["precision"] ?? 1;

                            // Check if anything has changed at all. If so, we
                            // need to redraw the act value limits.
                            const limitsHaveChanged =
                                this.vars.valueSettings.min !== min ||
                                this.vars.valueSettings.max !== max ||
                                this.vars.valueSettings.prewarn.min !== prewarnmin ||
                                this.vars.valueSettings.prewarn.max !== prewarnmax ||
                                this.vars.valueSettings.warn.min !== warnmin ||
                                this.vars.valueSettings.warn.max !== warnmax ||
                                this.vars.valueSettings.tolMin !== tolmin ||
                                this.vars.valueSettings.tolMax !== tolmax ||
                                this.vars.valueSettings.actValuePrecision !== precision;

                            this.vars.valueSettings.min = min;
                            this.vars.valueSettings.max = max;
                            this.vars.valueSettings.type = type;
                            this.vars.valueSettings.prewarn.min = prewarnmin;
                            this.vars.valueSettings.prewarn.max = prewarnmax;
                            this.vars.valueSettings.warn.min = warnmin;
                            this.vars.valueSettings.warn.max = warnmax;
                            this.vars.valueSettings.tolMin = tolmin;
                            this.vars.valueSettings.tolMax = tolmax;
                            this.vars.valueSettings.actValuePrecision = precision;
                            this.vars.valueSettings.precision = precision; // for nv formatOutput

                            // Make sure the relevant parts of the gauge are
                            // updated.
                            if (limitsHaveChanged) {
                                this.vars.drawFlags.actLimits = true;
                                this.vars.drawFlags.actValue = true;
                                this.vars.drawFlags.background = true;
                            }
                            this.vars.drawFlags.tolLimits = this.vars.drawFlags.tolLimits ||
                                this.vars.valueSettings.tolMin !== min ||
                                this.vars.valueSettings.tolMax !== max;
                        },
                        setLabel: this.setLabel.bind(this),
                        setUnitText: this.setUnitText.bind(this)
                    }));
                }

                this.vars.listeners.forEach((l) => l.enable());
            },
            onDisable: function onDisable() {
                if (!isReady(this)) {
                    return;
                }

                this.vars.listeners.forEach((l) => l.disable());

                stopDrawing(this);
                this.vars.resizeObserver?.disconnect(this.vars.elements.gaugeSvgSizeObserver);
                this.vars.intersectionObserver?.disconnect(this.vars.elements.svg);
                this.vars.mutationObserver?.disconnect(this.element);
                this.vars.subscriptions.forEach((s) => s.unlisten());
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function onLock() {
                shmi.addClass(this.element, 'locked');
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function onUnlock() {
                shmi.removeClass(this.element, 'locked');
            },

            getValue: function() {
                return this.vars.actValue.value;
            },

            setUnitText: function(unitText) {
                const self = this;

                if (self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-progress-info
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-progress-info",
 *     "name": null,
 *     "template": "custom/controls/iq-progress-info"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "item": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqProgressInfo", // control name in camel-case
        uiType = "iq-progress-info", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": `${uiType} iq-variant-01`,
        "name": null,
        "template": "default/iq-progress-info.iq-variant-01",
        "displayFormat": "($baseItem / $comparisonItem) - $percentValue%",
        "baseItem": null,
        "comparisonItem": null,
        "tooltip": null,
        "precision": -1,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "default-value": "---",

        "auto-precision": true,

        "animation-duration": 100,
        "fill-reverse": false,
        "fill-inverse": false
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Creates placeholder elements in the value element for the format string.
     *
     * @param {*} self
     * @param {string} displayFormat
     */
    function setupValueElements(self, displayFormat) {
        const { value: valueEl } = self.vars.dom;

        if (!valueEl) {
            return;
        }

        valueEl.innerHTML = displayFormat.
            replace(/[$]baseItem/g, `<span class="baseValue"></span>`).
            replace(/[$]comparisonItem/g, `<span class="compareValue"></span>`).
            replace(/[$]percentValue/g, `<span class="percentValue"></span>`);

        self.vars.placeholders = {
            baseItem: Array.from(valueEl.getElementsByClassName("baseValue")),
            comparisonItem: Array.from(valueEl.getElementsByClassName("compareValue")),
            percentValue: Array.from(valueEl.getElementsByClassName("percentValue"))
        };
    }

    /**
     * Sets values of placeholder elements.
     *
     * @param {*} self
     * @param {number} baseValue
     * @param {number} comparisonValue
     */
    function setPlaceholderValues(self, baseValue, comparisonValue) {
        const {
                nv: { formatOutput },
                iter: { iterateObject }
            } = self.imports,
            data = {
                baseItem: formatOutput(baseValue, self),
                comparisonItem: formatOutput(comparisonValue, self),
                percentValue: Math.round(baseValue / comparisonValue * 100)
            };

        if (!self.vars.placeholders) {
            return;
        }

        iterateObject(data, (value, key) => {
            const placeholders = self.vars.placeholders[key];
            if (Array.isArray(placeholders)) {
                placeholders.forEach((el) => el.textContent = value);
            }
        });
    }

    /**
     * Returns the bar fill percentage between 0 and 1 for the given value.
     *
     * @param {number} value
     * @param {number} comparisonValue
     * @param {boolean} [invert]
     * @returns {number} Bar fill percentage between 0 and 1.
     */
    function getFillPercentage(value, comparisonValue, invert) {
        const percentage = value / comparisonValue;

        if (isNaN(percentage)) {
            return 0;
        } else if (invert) {
            return 1 - Math.min(Math.max(percentage, 0), 1);
        }

        return Math.min(Math.max(percentage, 0), 1);
    }

    /**
     * Updates value display.
     *
     * @param {*} self
     */
    function updateWidgetText(self) {
        if (!self.vars.dom.value) {
            // Nothing to do
        } else if (self.getValue() === null || self.getComparisonValue() === null) {
            self.vars.dom.value.textContent = shmi.localize(self.config["default-value"]);
            self.vars.placeholders = null;
        } else {
            if (!self.vars.placeholders) {
                setupValueElements(self, self.config.displayFormat);
            }

            setPlaceholderValues(self, self.getValue(), self.getComparisonValue());
        }
    }

    /**
     * Updates bar.
     *
     * @param {*} self
     * @param {?number} param1.baseValue
     * @param {?number} param1.compValue
     */
    function updateWidgetBar(self, { baseValue, compValue }) {
        const valuePercentage = getFillPercentage(baseValue, compValue, self.config["fill-inverse"]);

        if (self.vars.dom.barContainer) {
            self.vars.dom.barContainer.style.setProperty("--internal-fill-level", `${valuePercentage * 100}%`);
        }
    }

    /**
     * Queues a widget update for the next animation frame. Will only queue one
     * update per frame.
     *
     * @param {*} self
     */
    function queueWidgetTextUpdate(self) {
        if (self.vars.rafId || !self.vars.dom.value) {
            return;
        }

        self.vars.rafId = shmi.raf(() => {
            updateWidgetText(self);

            self.vars.rafId = null;
        });
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            dom: {
                value: null,
                bar: null,
                track: null,
                barContainer: null
            },
            placeholders: null,
            valueSettings: {
                min: 0,
                max: 100
            },
            animationBundle: null,
            rafId: null,
            value: null,
            valueComp: null,
            listeners: [],
            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            iter: "visuals.tools.iterate",
            nv: "visuals.tools.numericValues",
            gfx: "visuals.gfx"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                // DOM
                Object.assign(self.vars.dom, {
                    value: shmi.getUiElement("value", self.element),
                    bar: shmi.getUiElement("bar", self.element),
                    track: shmi.getUiElement("track", self.element),
                    barContainer: shmi.getUiElement("bar-container", self.element)
                });

                self.imports.nv.initValueSettings(self);

                // Fill orientation
                if (self.vars.dom.bar && self.vars.dom.track) {
                    if (self.config["fill-reverse"]) {
                        shmi.addClass(self.vars.dom.bar, "reversed");
                        shmi.addClass(self.vars.dom.track, "reversed");
                    }

                    self.vars.animationBundle = new self.imports.gfx.AnimationBundle(updateWidgetBar.bind(null, self));
                    self.vars.animationBundle.prepare("baseValue", null, "linear");
                    self.vars.animationBundle.prepare("compValue", null, "linear");
                }

                updateWidgetText(self);
            },

            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                if (self.config.baseItem) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.baseItem, self));
                }

                if (self.config.comparisonItem) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.comparisonItem, {
                        setValue: self.onSetCompValue.bind(self)
                    }));
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.vars.rafId) {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = null;
                }

                if (self.vars.animationBundle) {
                    self.vars.animationBundle.cancel("baseValue");
                    self.vars.animationBundle.cancel("compValue");
                }

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            },

            /**
             * Retrieves current value of the progress info
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /**
             * Retrieves current comparison value of the progress info
             *
             * @return value - current comparison value
             */
            getComparisonValue: function() {
                const self = this;

                return self.vars.valueComp;
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.value === newValue) {
                    return;
                }

                self.vars.value = newValue;
                if (self.vars.animationBundle) {
                    self.vars.animationBundle.start(newValue, self.config["animation-duration"], "baseValue");
                }

                queueWidgetTextUpdate(self);
            },

            onSetCompValue: function(value, type) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.valueComp === newValue) {
                    return;
                }

                self.vars.valueComp = newValue;
                if (self.vars.animationBundle) {
                    self.vars.animationBundle.start(newValue, self.config["animation-duration"], "compValue");
                }

                queueWidgetTextUpdate(self);
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(...args) {
                const self = this;
                //const [min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision] = args;

                self.imports.nv.setProperties(self, args);
                queueWidgetTextUpdate(self);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-quality-display
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-quality-display",
 *     "name": null,
 *     "template": "custom/controls/iq-quality-display"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "item": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqQualityDisplay", // control name in camel-case
        uiType = "iq-quality-display", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": `${uiType} iq-variant-01`,
        "name": null,
        "template": "default/iq-quality-display.iq-variant-01",
        "item": null,
        "item-ctrl": null,
        "tooltip": null,
        "type": shmi.c("TYPE_INT"),
        "precision": 0,
        "min": 0,
        "max": 100,
        "label": "[Label]",
        "unit-text": "[Unit]",
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "default-value": "---",
        "show-icon": false,
        "show-text": true,
        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": false,
        "auto-type": true,
        "auto-min": true,
        "auto-max": true,
        "auto-label": true,
        "auto-unit-text": true,
        "auto-precision": true,

        "animation-duration": 100
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.dom.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined" || !self.config["show-text"]) {
            self.vars.label = "";
            self.vars.dom.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.dom.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.dom.unit) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined" || !self.config["show-text"]) {
            self.vars.unit = "";
            self.vars.dom.unit.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.dom.unit.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Returns the bar fill percentage between 0 and 1 for the given value. If
     * no value is given, the widgets current value is used instead.
     *
     * @param {*} self
     * @param {number} value
     * @returns {number} Bar fill percentage between 0 and 1.
     */
    function getFillPercentage(self, value) {
        const { min, max } = self.vars.valueSettings,
            percentage = (value - min) / (max - min);

        if (isNaN(percentage) || max === min) {
            return 0;
        }

        return Math.min(Math.max(percentage, 0), 1);
    }

    /**
     * Updates value display.
     *
     * @param {*} self
     */
    function updateWidgetText(self) {
        if (self.vars.dom.value) {
            if (self.getValue() === null) {
                self.vars.dom.value.textContent = shmi.localize(self.config["default-value"]);
            } else {
                self.vars.dom.value.textContent = self.imports.nv.formatOutput(self.getValue(), self);
            }
        }

        if (self.vars.dom.controlValue) {
            if (self.getControlValue() === null) {
                self.vars.dom.controlValue.textContent = shmi.localize(self.config["default-value"]);
            } else {
                self.vars.dom.controlValue.textContent = self.imports.nv.formatOutput(self.getControlValue(), self);
            }
        }
    }

    /**
     * Updates bar and control value position.
     *
     * @param {*} self
     * @param {?number} param1.actValue
     * @param {?number} param1.ctrlValue
     */
    function updateWidgetBars(self, { actValue, ctrlValue }) {
        const valuePercentage = getFillPercentage(self, actValue),
            valueControlPercentage = getFillPercentage(self, ctrlValue);

        const {
            barContainer: barContainerEl,
            indicator: indicatorEl,
            controlValue: controlValueEl
        } = self.vars.dom;

        if (barContainerEl && controlValueEl && indicatorEl) {
            barContainerEl.style.setProperty("--internal-fill-level", `${valuePercentage * 100}%`);
            barContainerEl.style.setProperty("--internal-fill-width", `${Math.abs(valuePercentage - valueControlPercentage) * 100}%`);

            if (valueControlPercentage !== self.vars.cache.controlValue || self.vars.barBoundingRect !== self.vars.cache.barBoundingRect) {
                self.element.style.setProperty("--internal-fill-level-set", `${valueControlPercentage * 100}%`);
                self.element.style.setProperty("--internal-bar-offset-v", `${self.vars.barBoundingRect.height * (1 - valueControlPercentage)}px`);
                self.element.style.setProperty("--internal-bar-offset-h", `${self.vars.barBoundingRect.width * valueControlPercentage}px`);
                self.vars.cache.controlValue = valueControlPercentage;
                self.vars.cache.barBoundingRect = self.vars.barBoundingRect;
            }

            const isNegative = valuePercentage - valueControlPercentage < 0;
            if (isNegative === self.vars.cache.isNegative) {
                return;
            }

            self.vars.cache.isNegative = isNegative;
            if (isNegative) {
                shmi.addClass(barContainerEl, "negative");
            } else {
                shmi.removeClass(barContainerEl, "negative");
            }
        }
    }

    /**
     * Queues a widget update for the next animation frame. Will only queue one
     * update per frame.
     *
     * @param {*} self
     */
    function queueWidgetTextUpdate(self) {
        if (self.vars.rafId) {
            return;
        }

        self.vars.rafId = shmi.raf(() => {
            updateWidgetText(self);

            self.vars.rafId = null;
        });
    }

    /**
     * Computes orientation and anchor of the gauge bar.
     *
     * @param {*} self
     * @returns {object}
     */
    function getFillProperties(self) {
        if (!self.vars.dom.bar) {
            return null;
        }

        const computed = getComputedStyle(self.vars.dom.bar),
            [transformOriginX, transformOriginY] = computed.transformOrigin.replace("px", "").split(" ").map(parseFloat),
            isHorizontalScaling = transformOriginY <= parseFloat(computed.height.replace("px", "")) / 2,
            fromLeft = transformOriginX === 0,
            fromTop = transformOriginY === 0;

        return {
            orientation: isHorizontalScaling ? "horizontal" : "vertical",
            reverse: isHorizontalScaling ? !fromTop : !fromLeft,
            anchor: (() => {
                if (isHorizontalScaling) {
                    return fromLeft ? "left" : "right";
                } else {
                    return fromTop ? "top" : "bottom";
                }
            })()
        };
    }

    /**
     * Creates a Visuals token-like observer that listens for changes that
     * require the widget to be updated.
     *
     * @param {*} self
     * @param {function} callback
     */
    function makeUpdateToken(self, callback) {
        let throttled = false;

        const throttledCallback = () => {
                self.vars.barBoundingRect = self.vars.dom.barContainer.getBoundingClientRect();
                throttled = false;
                Promise.resolve().then(() => {
                    if (!throttled) {
                        callback();
                        throttled = true;
                    }
                });
            },
            // We only use the ResizeObserver if it's supported. Otherwise we
            // have to rely on the `resize` event only :(.
            // eslint-disable-next-line compat/compat
            resizeObserver = window.ResizeObserver ? new ResizeObserver(throttledCallback) : null;

        window.addEventListener("resize", throttledCallback);
        if (resizeObserver && self.vars.dom.barContainer) {
            resizeObserver.observe(self.vars.dom.barContainer);
        }

        return {
            unlisten: () => {
                window.removeEventListener("resize", throttledCallback);
                if (resizeObserver && self.vars.dom.barContainer) {
                    resizeObserver.unobserve(self.vars.dom.barContainer);
                }
            }
        };
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            dom: {
                value: null,
                bar: null,
                barContainer: null,
                unit: null,
                label: null,
                icon: null,
                indicator: null,
                indicatorLine: null,
                controlValue: null
            },
            valueSettings: {
                min: 0,
                max: 100
            },
            cache: {
                barBoundingRect: {
                    width: null,
                    height: null
                },
                controlValue: null,
                isNegative: null
            },
            barBoundingRect: {
                width: 0,
                height: 0
            },
            animationBundle: null,
            rafId: null,
            value: null,
            valueCtrl: null,
            orientationInfo: null,
            listeners: [],
            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues",
            gfx: "visuals.gfx"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                // DOM
                Object.assign(self.vars.dom, {
                    value: shmi.getUiElement("value", self.element),
                    bar: shmi.getUiElement("bar", self.element),
                    barContainer: shmi.getUiElement("bar-container", self.element),
                    unit: shmi.getUiElement("unit", self.element),
                    label: shmi.getUiElement("label", self.element),
                    icon: shmi.getUiElement("icon", self.element),
                    indicator: shmi.getUiElement("indicator", self.element),
                    indicatorLine: shmi.getUiElement("indicator-line", self.element),
                    controlValue: shmi.getUiElement("control-value", self.element)
                });

                self.imports.nv.initValueSettings(self);

                // Icon
                if (self.vars.dom.icon && self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.dom.icon, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                    shmi.addClass(self.vars.dom.icon, "inlined");
                } else if (self.vars.dom.icon && self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.dom.icon, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);
                if (self.config['unit-scale']) {
                    self.config['unit-scale'] = parseFloat(shmi.localize(self.config['unit-scale']));
                }

                // Label
                setLabelImpl(self, self.config.label);

                // Fill orientation
                if (self.vars.dom.bar) {
                    self.vars.orientationInfo = getFillProperties(self);
                }

                self.vars.animationBundle = new self.imports.gfx.AnimationBundle(updateWidgetBars.bind(null, self));
                self.vars.animationBundle.prepare("actValue", null, "linear");
                self.vars.animationBundle.prepare("ctrlValue", null, "linear");

                updateWidgetText(self);
            },

            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.tokens.push(makeUpdateToken(self, () => {
                    queueWidgetTextUpdate(self);
                    self.vars.animationBundle.refresh();
                }));

                if (self.config.item) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.item, self));
                }

                if (self.config["item-ctrl"]) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["item-ctrl"], {
                        setValue: self.onSetCtrlValue.bind(self)
                    }));
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.vars.rafId) {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = null;
                }

                self.vars.animationBundle.cancel("actValue");
                self.vars.animationBundle.cancel("ctrlValue");

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            },

            /**
             * Retrieves current value of the quality display
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /**
             * Retrieves current control value of the quality display
             *
             * @return value - current control value
             */
            getControlValue: function() {
                const self = this;

                return self.vars.valueCtrl;
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.value === newValue) {
                    return;
                }

                self.vars.value = newValue;
                self.vars.animationBundle.start(newValue, self.config["animation-duration"], "actValue");
                queueWidgetTextUpdate(self);
            },

            onSetCtrlValue: function(value, type) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.valueCtrl === newValue) {
                    return;
                }

                self.vars.valueCtrl = newValue;
                self.vars.animationBundle.start(newValue, self.config["animation-duration"], "ctrlValue");
                queueWidgetTextUpdate(self);
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(...args) {
                const self = this;
                //const [min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision] = args;

                self.imports.nv.setProperties(self, args);
                self.vars.animationBundle.refresh();
                queueWidgetTextUpdate(self);
            },

            setUnitText: function(unitText) {
                const self = this;

                if (self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-radial-gauge
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-radial-gauge",
 *     "template": "default/iq-radial-gauge.iq-variant-01",
 *     "name": "iq-radial-gauge",
 *     "ui": "iq-radial-gauge",
 *     "label": "[label]",
 *     "item": null,
 *     "min": null,
 *     "max": null,
 *     "unit-text": "[Unit]",
 *     "auto-label": true,
 *     "auto-unit-text": true,
 *     "auto-min": true,
 *     "auto-max": true,
 *     "type": 2,
 *     "auto-type": true,
 *     "decimal-delimiter": "${DECIMAL_DELIMITER}",
 *     "precision": 0,
 *     "auto-precision": true,
 *     "icon-src": null,
 *     "icon-class": null,
 *     "tooltip": null,
 *     "animation-duration": 100,
 *     "disable-alarm-styling": false,
 *     "arc-angle": 270,
 *     "arc-rotation": 135,
 *     "show-value": true,
 *     "default-value": "---",
 *     "arc-fill-reverse": false,
 *     "arc-fill-inverse": false,
 *     "value-labels": 0
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * tooltip {string}: Tooltip
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqRadialGauge", // control name in camel-case
        uiType = "iq-radial-gauge", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": uiType,
        "template": "default/iq-radial-gauge.iq-variant-01",
        "name": uiType,
        "ui": uiType,
        "label": "[label]",
        "item": null,
        "min": null,
        "max": null,
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "auto-min": true,
        "auto-max": true,
        "type": 2,
        "auto-type": true,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "precision": 0,
        "auto-precision": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "animation-duration": 100,
        "disable-alarm-styling": false,
        "arc-angle": 270,
        "arc-rotation": 135,
        "show-value": true,
        "default-value": "---",
        "arc-fill-reverse": false,
        "arc-fill-inverse": false,
        "value-labels": 0
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${uiType}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${uiType}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${uiType}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${uiType}]`)
    };

    const TICK_LENGTH = 6,
        TICK_LABEL_DISTANCE = 6,
        TICK_GAP = 6,
        VALUE_GAP = 8;

    /**
     * Returns a random id.
     *
     * @returns {string} random id
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.elements.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.elements.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.elements.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.elements.labelUnit) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined") {
            self.vars.elements.labelUnit.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            const localizedUnitText = shmi.localize(unitText);
            self.vars.elements.labelUnit.textContent = localizedUnitText;
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Ensures the cache object is present and some commonly used variables are
     * set.
     *
     * @param {*} self Reference to the control.
     */
    function checkCache(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }

        if (!self.vars.cache.gaugeWidth) {
            self.vars.cache.gaugeWidth = getStylePropertyValue(self.vars.elements.gaugePathFill, "width");
            self.vars.corners.outer = getStylePropertyValue(self.element, "--corner-radius-outer");
            self.vars.corners.inner = getStylePropertyValue(self.element, "--corner-radius-inner");

            if (self.vars.corners.outer > self.vars.cache.gaugeWidth / 2) {
                self.vars.corners.outer = self.vars.cache.gaugeWidth / 2;
            }
            if (self.vars.corners.inner > self.vars.cache.gaugeWidth / 2) {
                self.vars.corners.inner = self.vars.cache.gaugeWidth / 2;
            }
            if (self.vars.corners.inner > self.vars.corners.outer) {
                self.vars.corners.inner = self.vars.corners.outer;
            }

            self.vars.drawFlags.background = true;
        }
    }

    /**
     * Returns the value of a computed style property.
     *
     * @param {SVGElement} element The SVG element to get a property for.
     * @param {string} property The property to look for
     * @returns {number} value of the property in pixel.
     */
    function getStylePropertyValue(element, property) {
        const styles = getComputedStyle(element);
        const propValue = styles.getPropertyValue(property);

        if (!propValue || propValue === "auto") {
            return 0;
        }

        const propInfo = /^\s?(-?\d+(?:.\d+)?)\s*(px|em|rem|%)?\s?$/.exec(propValue);
        if (!propInfo) {
            logger.error("Failed to parse property string", propValue);
            return 0;
        }
        const [, propStr, propUnit] = propInfo;

        switch (propUnit) {
        case "px":
        case undefined:
            return parseFloat(propStr);
        default:
            logger.error("Encountered unsupported unit", propUnit);
            return 0;
        }
    }

    /**
     * @typedef {object} BoundingBoxWithRadius
     *
     * @property {number} top
     * @property {number} left
     * @property {number} bottom
     * @property {number} right
     * @property {number} centerX
     * @property {number} centerY
     * @property {number} width
     * @property {number} height
     * @property {number} radius
     */

    /**
     * Checks whether the controls main element dimensions have changed.
     *
     * @param {*} self Reference to the control.
     * @returns {boolean} `true` if a change was detected, `false` else.
     */
    function haveDisplayPropertiesChanged(self) {
        if (!self.vars.cache) {
            return true;
        } else if (!self.vars.cache.display) {
            return true;
        }
        return false;
    }

    /**
     * Converts degrees to radiants.
     *
     * @param {number} deg
     * @returns {number}
     */
    function degToRad(deg) {
        return Math.PI * (deg / 180);
    }

    /**
     * Returns min if value is lower than min, max if the value is larger than
     * max, or value otherwise.
     *
     * @param {number} min Lower bound of the interval
     * @param {number} value Value to clamp.
     * @param {number} max Upper bound of the interval
     * @returns {number} min if value is lower than min, max if the value is
     *  larger than max, or value otherwise.
     */
    function clamp(min, value, max) {
        return Math.max(min, Math.min(value, max));
    }

    /**
     * Returns coordinates for a value on a given arc of radius 1.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {number} value Value to return the coordinate for.
     * @returns {{ sin: number, cos: number }}
     */
    function getCircleCoordsForValue(self, arcStart, arcEnd, value) {
        const theta = getAngleForValue(self, arcStart, arcEnd, value);

        return {
            sin: Math.sin(theta),
            cos: Math.cos(theta)
        };
    }

    /**
     * Returns coordinates for a value on a given arc.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     * @param {number} value Value to return the coordinate for.
     * @param {number} radiusOffset
     * @returns {{ x: number, y: number }}
     */
    function getPointAtValue(self, arcStart, arcEnd, gaugeBoundingBox, value, radiusOffset = 0) {
        const radius = gaugeBoundingBox.radius + radiusOffset,
            theta = getAngleForValue(self, arcStart, arcEnd, value);

        const x = gaugeBoundingBox.centerX + Math.cos(theta) * radius,
            y = gaugeBoundingBox.centerY + Math.sin(theta) * radius;

        if (x === Infinity || y === Infinity || isNaN(x) || isNaN(y)) {
            return {
                x: null,
                y: null
            };
        }

        return {
            x,
            y
        };
    }

    /**
     * Returns the offset for an object considering its current rotation
     *
     * @param {number} rotation Rotation
     * @param {number} offset Offset to return the coordinate for.
     * @returns {{ x: number, y: number }}
     */
    function getRotatedOffset(rotation, offset) {
        return {
            x: Math.sin(rotation) * offset,
            y: -(Math.cos(rotation) * offset)
        };
    }

    /**
     * Returns coordinates for an angle on an arc.
     *
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     * @param {number} radius arc radius
     * @param {number} theta Angle to return the coordinate for.
     * @returns {{ x: number, y: number }}
     */
    function getPointAtAngle(gaugeBoundingBox, radius, theta) {
        return {
            x: gaugeBoundingBox.centerX + Math.cos(theta) * radius,
            y: gaugeBoundingBox.centerY + Math.sin(theta) * radius
        };
    }

    /**
     * Returns the angle from the origin of the arc for the given value. The
     * value is clamped to the gauges limits.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {number} value Value to return the angle for.
     * @returns {number}
     */
    function getAngleForValue(self, arcStart, arcEnd, value, reversed) {
        const { valueSettings } = self.vars;

        // Make sure the value stays inside our bounds.
        value = clamp(valueSettings.min, value, valueSettings.max);
        let flipArcValues = !!self.config["arc-fill-inverse"];

        if (reversed) {
            flipArcValues = !flipArcValues;
        }

        if (flipArcValues) {
            return arcEnd - (arcEnd - arcStart) / (valueSettings.max - valueSettings.min) * (value - valueSettings.min);
        } else {
            return arcStart + (arcEnd - arcStart) / (valueSettings.max - valueSettings.min) * (value - valueSettings.min);
        }
    }

    /**
     * Returns an SVG path instruction string for the line.
     *
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {BoundingBox} gaugeBoundingBox
     * @param {number} valueStart
     * @param {number} valueEnd
     * @param {number} [r] Radius for the arc rounded corners
     * @param {number} [radiusOffset]
     * @param {boolean} [invertLong] Inverts the "long" flag of the emitted arc
     * @param {boolean} [isFlippedAct] Draw the Arc flipped (from end to value instead of start to value)
     * @returns {string} SVG path instruction string for the arc.
     */
    function renderRoundedArc(self, arcStart, arcEnd, gaugeBoundingBox, valueStart, valueEnd, r = 0, radiusOffset = 0, invertLong = false, isFlippedAct = false) {
        const { valueSettings } = self.vars;
        let flipArcValues = !!self.config["arc-fill-inverse"];
        const outerRadius = gaugeBoundingBox.radius + radiusOffset + self.vars.cache.gaugeWidth / 2 - TICK_LENGTH,
            innerRadius = gaugeBoundingBox.radius + radiusOffset - self.vars.cache.gaugeWidth / 2 - TICK_LENGTH;

        if (isFlippedAct) {
            flipArcValues = !flipArcValues;
        }

        const theta = getAngleForValue(self, arcStart, arcEnd, valueEnd, isFlippedAct),
            arcOffs = getRotatedOffset((flipArcValues ? theta : arcStart) + 0.5 * Math.PI, self.vars.cache.gaugeWidth / 2),
            valOffsEnd = getRotatedOffset((flipArcValues ? arcEnd : theta) + 0.5 * Math.PI, self.vars.cache.gaugeWidth / 2);

        // Compute the two endpoints of the original centered line from start to end.
        const { x: x1, y: y1 } = getPointAtValue(self, arcStart, arcEnd, gaugeBoundingBox, flipArcValues ? (isFlippedAct ? valueSettings.max - valueEnd + valueSettings.min : valueEnd) : (isFlippedAct ? valueSettings.max : valueSettings.min), radiusOffset - TICK_LENGTH),
            { x: x2, y: y2 } = getPointAtValue(self, arcStart, arcEnd, gaugeBoundingBox, flipArcValues ? (isFlippedAct ? valueSettings.max : valueSettings.min) : (isFlippedAct ? valueSettings.max - valueEnd + valueSettings.min : valueEnd), radiusOffset - TICK_LENGTH);

        if (x1 === null || x2 === null || y1 === null || y2 === null) {
            return [""];
        }

        const length = (flipArcValues ? arcEnd - theta : theta - arcStart) * outerRadius,
            newStart = flipArcValues ? arcEnd - (length - r) / outerRadius : theta - (length - r) / outerRadius,
            newEnd = flipArcValues ? theta + (length - r) / outerRadius : arcStart + (length - r) / outerRadius,
            // Check if the radius is > 180deg. If so, we need to set the long
            // flag for the SVG.
            long = Math.abs(newEnd - newStart) > Math.PI;

        const newStartPoint = getPointAtAngle(gaugeBoundingBox, outerRadius, newStart),
            newEndPoint = getPointAtAngle(gaugeBoundingBox, outerRadius, newEnd),
            newStartPointInner = getPointAtAngle(gaugeBoundingBox, innerRadius, newStart),
            newEndPointInner = getPointAtAngle(gaugeBoundingBox, innerRadius, newEnd);

        // calculate the offset for both arcs
        const movR = getRotatedOffset(flipArcValues ? theta : arcStart, r);
        const movR2 = getRotatedOffset(flipArcValues ? arcEnd : theta, r);

        return [
            `M ${x1 - arcOffs.x - movR.y}  ${y1 - arcOffs.y + movR.x} `,
            `L ${x1 + arcOffs.x + movR.y}  ${y1 + arcOffs.y - movR.x} `,
            `A ${r} ${r} 0 0 1 ${newStartPoint.x} ${newStartPoint.y}`,
            `A ${outerRadius} ${outerRadius} 0 ${(long !== invertLong) ? 1 : 0} 1 ${newEndPoint.x} ${newEndPoint.y}`,
            `A ${r} ${r} 0 0 1 ${x2 + valOffsEnd.x + movR2.y} ${y2 + valOffsEnd.y - movR2.x}`,
            `L ${x2 - valOffsEnd.x - movR2.y}  ${y2 - valOffsEnd.y + movR2.x} `,
            `A ${r} ${r} 0 0 1 ${newEndPointInner.x} ${newEndPointInner.y}`,
            `A ${innerRadius} ${innerRadius} 0 ${(long !== invertLong) ? 1 : 0} 0 ${newStartPointInner.x} ${newStartPointInner.y}`,
            `A ${r} ${r} 0 0 1 ${x1 - arcOffs.x - movR.y}  ${y1 - arcOffs.y + movR.x}`,
            `z`
        ].join(" ");
    }

    /**
     * Returns an SVG path instruction string for the arc.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {BoundingBox} gaugeBoundingBox
     * @param {number} valueStart
     * @param {number} valueEnd
     * @param {number} [radiusOffset]
     * @param {boolean} [invertLong] Inverts the "long" flag of the emitted arc
     *  SVG instruction.
     * @returns {string} SVG path instruction string for the arc.
     */
    function renderArc(self, arcStart, arcEnd, gaugeBoundingBox, valueStart, valueEnd, radiusOffset = 0, invertLong = false) {
        const flipArcValues = !!self.config["arc-fill-inverse"],
            radius = gaugeBoundingBox.radius + radiusOffset,
            theta1 = getAngleForValue(self, arcStart, arcEnd, flipArcValues ? valueEnd : valueStart),
            theta2 = getAngleForValue(self, arcStart, arcEnd, flipArcValues ? valueStart : valueEnd),
            // Check if the radius is > 180deg. If so, we need to set the long
            // flag for the SVG.
            long = Math.abs(theta2 - theta1) > Math.PI;

        // Compute the two endpoints of the line.
        const { x: x1, y: y1 } = getPointAtValue(self, arcStart, arcEnd, gaugeBoundingBox, flipArcValues ? valueEnd : valueStart, radiusOffset),
            { x: x2, y: y2 } = getPointAtValue(self, arcStart, arcEnd, gaugeBoundingBox, flipArcValues ? valueStart : valueEnd, radiusOffset);

        if (x1 === null || x2 === null || y1 === null || y2 === null) {
            return [""];
        }

        return [
            `M ${x1} ${y1}`,
            `A ${radius} ${radius} 0 ${(long !== invertLong) ? 1 : 0} 1 ${x2} ${y2}`
        ].join(" ");
    }

    /**
     * Initializes the colored value range lines of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {Set} ticks Set of values where values for ticks are to be
     *  inserted.
     */
    function initGaugeValueRanges(self, ticks) {
        const {
            elements: { gaugePathValueRangeNominal, gaugePathValueRangePrewarn, gaugePathValueRangeWarn },
            valueSettings
        } = self.vars;
        const clampValue = (value) => clamp(valueSettings.min, value, valueSettings.max);

        const newRanges = [
            // Nominal range (green)
            {
                element: gaugePathValueRangeNominal,
                ranges: [
                    {
                        from: clampValue(valueSettings.prewarn.min ?? valueSettings.warn.min ?? valueSettings.min),
                        to: clampValue(valueSettings.prewarn.max ?? valueSettings.warn.max ?? valueSettings.max)
                    }
                ]
            },
            // Prewarn range (yellow)
            {
                element: gaugePathValueRangePrewarn,
                ranges: [
                    {
                        from: clampValue(valueSettings.warn.min ?? valueSettings.min),
                        to: clampValue(valueSettings.prewarn.min ?? valueSettings.warn.min ?? valueSettings.min)
                    },
                    {
                        from: clampValue(valueSettings.prewarn.max ?? valueSettings.warn.max ?? valueSettings.max),
                        to: clampValue(valueSettings.warn.max ?? valueSettings.max)
                    }
                ]
            },
            // Warn range (red)
            {
                element: gaugePathValueRangeWarn,
                ranges: [
                    {
                        from: valueSettings.min,
                        to: clampValue(valueSettings.warn.min ?? valueSettings.min)
                    },
                    {
                        from: clampValue(valueSettings.warn.max ?? valueSettings.max),
                        to: valueSettings.max
                    }
                ]
            }
        ];
        newRanges.forEach(({ element, ranges }) => {
            // Check if there are enough SVG elements for us. This check fails
            // if the template is faulty.
            if (!element || element.children.length !== ranges.length) {
                return;
            }

            ranges.forEach(({ from, to }, idx) => {
                ticks.add(from);
                ticks.add(to);
            });
        });
        return newRanges;
    }

    /**
     * Updates the colored value range lines of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the arc
     * @param {number} arcEnd Ending angle of the arc
     * @param {BoundingBox} gaugeBoundingBox
     * @param {Set} newRanges Set of values where values for ticks are to be
     *  positioned.
     */
    function positionGaugeValueRanges(self, arcStart, arcEnd, gaugeBoundingBox, newRanges) {
        const {
            cache: { gaugeWidth },
            valueSettings
        } = self.vars;

        newRanges.forEach(({ element, ranges }) => {
            // Check if there are enough SVG elements for us. This check fails
            // if the template is faulty.
            if (!element || element.children.length !== ranges.length) {
                return;
            }

            ranges.forEach(({ from, to, invert }, idx) => {
                const length = (arcEnd - arcStart) * gaugeBoundingBox.radius / (valueSettings.max - valueSettings.min),
                    gapOffset = TICK_GAP / length;

                if (from >= to || from + gapOffset >= to - gapOffset || (valueSettings.prewarn.min === null && valueSettings.prewarn.max === null && valueSettings.warn.min === null && valueSettings.warn.max === null)) {
                    // The range has zero length. Hide the element.
                    element.children[idx].removeAttribute("d");
                } else {
                    // Render the arc
                    element.children[idx].setAttribute("d", renderArc(self, arcStart, arcEnd, gaugeBoundingBox, from + gapOffset, to - gapOffset, gaugeWidth / 2, invert ?? false));
                }
            });
        });
    }

    /**
     * Updates the act value and its bar on the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the gauge arc in radiants.
     * @param {number} arcEnd Ending angle of the gauge arc in radiants.
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     */
    function drawGaugeActValue(self, arcStart, arcEnd, gaugeBoundingBox) {
        const {
            corners: { inner },
            elements: { gaugeMaskPath },
            animators: { actValue: { value: actValueValue } },
            valueSettings
        } = self.vars;

        // Define a helper bind so we don't have to throw so many arguments around all the time.
        /** @type {(valueStart: number, valueEnd: number, r?: number, radiusOffset?: number, invertLong?: boolean, isFlippedAct?: boolean) => string} */
        const renderArcHelper = renderRoundedArc.bind(null, self, arcStart, arcEnd, gaugeBoundingBox);

        // Add a mask for the gauge that will be transformed to represent the current act value
        if (self.config["arc-fill-reverse"]) {
            gaugeMaskPath.setAttribute("d", renderArcHelper(valueSettings.min, valueSettings.max + valueSettings.min - actValueValue, inner, 0, false, true));
        } else {
            gaugeMaskPath.setAttribute("d", renderArcHelper(valueSettings.min, actValueValue, inner, 0, false, false));
        }
    }

    /**
     * Updates the tolerance limit lines on the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the gauge arc in radiants.
     * @param {number} arcEnd Ending angle of the gauge arc in radiants.
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     */
    function drawGaugeTolerances(self, arcStart, arcEnd, gaugeBoundingBox) {
        const {
            cache: { gaugeWidth },
            elements: { gaugeLineToleranceMin, gaugeLineToleranceMax },
            valueSettings: { min, max },
            valueSettings
        } = self.vars;

        /** @type {(value: number, orthogonalOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, arcStart, arcEnd, gaugeBoundingBox);

        if (valueSettings.tolMin === null || valueSettings.tolMin < min) {
            // No min value or value outside of display limits. Hide the limit
            // line.
            gaugeLineToleranceMin.removeAttribute("x1");
            gaugeLineToleranceMin.removeAttribute("y1");
            gaugeLineToleranceMin.removeAttribute("x2");
            gaugeLineToleranceMin.removeAttribute("y2");
        } else {
            // Compute the two points for our tolerance value limit line. The line
            // spans the entire gauge width and extends a bit further
            // "inwards". The hidden marker is positioned where the set value
            // marker would be, if it was at the lower bound.
            const pStart = getPointAtValueHelper(valueSettings.tolMin, -gaugeWidth / 2 - TICK_LABEL_DISTANCE),
                pEnd = getPointAtValueHelper(valueSettings.tolMin, gaugeWidth / 2);

            // Limit line.
            gaugeLineToleranceMin.setAttribute("x1", pStart.x ? pStart.x : 0);
            gaugeLineToleranceMin.setAttribute("y1", pStart.y ? pStart.y : 0);
            gaugeLineToleranceMin.setAttribute("x2", pEnd.x ? pEnd.x : 0);
            gaugeLineToleranceMin.setAttribute("y2", pEnd.y ? pEnd.y : 0);
        }

        if (valueSettings.tolMax === null || valueSettings.tolMax > max) {
            // No max value or value outside of display limits. Hide the limit
            // line.
            gaugeLineToleranceMax.removeAttribute("x1");
            gaugeLineToleranceMax.removeAttribute("y1");
            gaugeLineToleranceMax.removeAttribute("x2");
            gaugeLineToleranceMax.removeAttribute("y2");
        } else {
            // Compute the two points for our tolerance value limit line. The line
            // spans the entire gauge width and extends a bit further
            // "inwards". The hidden marker is positioned where the set value
            // marker would be, if it was at the upper bound.
            const pStart = getPointAtValueHelper(valueSettings.tolMax, -gaugeWidth / 2 - TICK_LABEL_DISTANCE),
                pEnd = getPointAtValueHelper(valueSettings.tolMax, gaugeWidth / 2);

            // Limit line.
            gaugeLineToleranceMax.setAttribute("x1", pStart.x ? pStart.x : 0);
            gaugeLineToleranceMax.setAttribute("y1", pStart.y ? pStart.y : 0);
            gaugeLineToleranceMax.setAttribute("x2", pEnd.x ? pEnd.x : 0);
            gaugeLineToleranceMax.setAttribute("y2", pEnd.y ? pEnd.y : 0);
        }
    }

    /**
     * Updates the value tick group of the SVG. SVG elements may be generated
     * in the group, if it does not have enough children for all ticks to be
     * displayed.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the gauge arc in radiants.
     * @param {number} arcEnd Ending angle of the gauge arc in radiants.
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     * @param {Set} ticks Set of values where ticks are to be displayed at.
     */
    function drawGaugeValueRangeTicks(self, arcStart, arcEnd, gaugeBoundingBox, ticks) {
        const {
            cache: { gaugeWidth },
            elements: { gaugePathValueRangeTicks },
            valueSettings
        } = self.vars;

        /** @type {(value: number, radiusOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, arcStart, arcEnd, gaugeBoundingBox);

        let currentTick = 0;
        ticks.forEach((value) => {
            if (valueSettings.min > value || value > valueSettings.max) {
                // Don't create graphical markers for values outside of the arc.
                return;
            }

            // Compute the starting and ending point of the current tick. The
            // tick is to overlap the value range bars, so we need to account
            // for that.
            const pStart = getPointAtValueHelper(value, gaugeWidth / 2 - 1),
                pEnd = getPointAtValueHelper(value, gaugeWidth / 2 + TICK_LENGTH);

            // If there's not enough children in the group, generate more.
            while (gaugePathValueRangeTicks.children.length <= currentTick) {
                gaugePathValueRangeTicks.append(document.createElementNS("http://www.w3.org/2000/svg", "line"));
            } // while ( ... )

            /** @type {SVGElement} */
            const lineElement = gaugePathValueRangeTicks.children[currentTick];
            lineElement.setAttribute("x1", pStart.x ? pStart.x : 0);
            lineElement.setAttribute("y1", pStart.y ? pStart.y : 0);
            lineElement.setAttribute("x2", pEnd.x ? pEnd.x : 0);
            lineElement.setAttribute("y2", pEnd.y ? pEnd.y : 0);

            ++currentTick;
        });

        // Hide elements not needed right now. We don't outright remove them
        // because we might need them later on and can do so faster if they
        // already exist.
        for (let i = currentTick; i < gaugePathValueRangeTicks.children.length; ++i) {
            /** @type {SVGElement} */
            const lineElement = gaugePathValueRangeTicks.children[i];

            lineElement.removeAttribute("x1");
            lineElement.removeAttribute("y1");
            lineElement.removeAttribute("x2");
            lineElement.removeAttribute("y2");
        } // for ( ... )
    }

    /**
     * Draws the value labels of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {Set} ticks Set of values where ticks are to be displayed at.
     * @param {Set} tolTicks Set of values where tolerance ticks are to be displayed at.
     */
    function drawGaugeValueRangeLabels(self, ticks, tolTicks) {
        const {
            elements: { gaugePathValueRangeLabels, gaugePathTolRangeLabels },
            valueSettings
        } = self.vars;

        /** @type {{left: number; right: number; top: number; bottom: number; value: number; width: number; height: number; element: SVGGraphicsElement}[]} */
        let valueBoxes = [],
            tolBoxes = [];

        let currentTick = 0;
        const largestBox = {
            width: 0,
            height: 0
        };

        function addValueBoxes(targetLabels, targetBoxes, value) {
            // Don't create labels for ticks outside the display range.
            if (valueSettings.min > value || value > valueSettings.max) {
                return;
            }

            // If there's not enough children in the group, generate more.
            while (targetLabels.children.length <= currentTick) {
                targetLabels.append(document.createElementNS("http://www.w3.org/2000/svg", "text"));
            } // while ( ... )

            /** @type {SVGGraphicsElement} */
            const textElement = targetLabels.children[currentTick];
            if (valueSettings["decimal-delimiter"]) {
                textElement.textContent = valueSettings.actValuePrecision >= 0 ? value.toFixed(valueSettings.actValuePrecision).replace(".", valueSettings["decimal-delimiter"]) : value.toString();
            } else {
                textElement.textContent = valueSettings.actValuePrecision >= 0 ? value.toFixed(valueSettings.actValuePrecision) : value.toString();
            }

            const textBB = textElement.getBBox();

            targetBoxes.push({
                element: textElement,
                left: null,
                top: null,
                right: null,
                bottom: null,
                width: textBB.width,
                height: textBB.height,
                value
            });
            if (textBB.width > largestBox.width) {
                largestBox.width = textBB.width;
            }
            if (textBB.height > largestBox.height) {
                largestBox.height = textBB.height;
            }

            ++currentTick;
        }

        tolTicks.forEach((value) => {
            addValueBoxes(gaugePathTolRangeLabels, tolBoxes, value);
        });
        currentTick = 0;
        ticks.forEach((value) => {
            addValueBoxes(gaugePathValueRangeLabels, valueBoxes, value);
        });

        // Remove elements not needed right now. We don't outright remove them
        // because we might need them later on and can do so faster if they
        // already exist.
        for (let i = currentTick; i < gaugePathValueRangeLabels.children.length; ++i) {
            /** @type {SVGElement} */
            const textElement = gaugePathValueRangeLabels.children[i];
            textElement.textContent = "";
            textElement.removeAttribute("x");
            textElement.removeAttribute("y");
        } // for ( ... )

        return {
            largestBox,
            valueBoxes,
            tolBoxes
        };
    }

    /**
     * Positions a value or tolerance label box.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the gauge arc in radiants.
     * @param {number} arcEnd Ending angle of the gauge arc in radiants.
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     * @param {valueBox} valueBox value box to position
     * @param {boolean} isTolBox is tolerance or value box
     */
    function doPositioning(self, arcStart, arcEnd, gaugeBoundingBox, valueBox, isTolBox) {
        const { getAnchorCoordinateForRect } = shmi.requires("iqGauge.gaugeHelpers"),
            {
                cache: { gaugeWidth }
            } = self.vars;

        /** @type {(value: number, radiusOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, arcStart, arcEnd, gaugeBoundingBox);

        // Point to anchor the label at.
        const coords = getPointAtValueHelper(valueBox.value, isTolBox ? -(gaugeWidth / 2 + TICK_LENGTH + TICK_LABEL_DISTANCE) : gaugeWidth / 2 + TICK_LENGTH + TICK_LABEL_DISTANCE);

        // Value labels are positioned orthogonal to the gauge (in
        // line"ish" with the ticks themselves). We need to compute the
        // anchor point on the text itself.
        //
        // Treat the text's bounding box as an ellipsis and anchor the text
        // on the side facing the bar.
        const offsetMult = getCircleCoordsForValue(self, arcStart, arcEnd, valueBox.value);

        // Compute the anchor. The text alignment is such that the origin
        // is in the center of the bounding box.
        const { x: offsetX, y: offsetY } = getAnchorCoordinateForRect(valueBox.width, valueBox.height, offsetMult.sin, offsetMult.cos);

        if (isNaN(coords.x) || isNaN(coords.y) || isNaN(offsetX) || isNaN(offsetY)) { // will be hidden later
            return;
        }

        valueBox.left = isTolBox ? coords.x - (valueBox.width / 2) - offsetX : coords.x - (valueBox.width / 2) + offsetX;
        valueBox.top = isTolBox ? coords.y - (valueBox.height / 2) - offsetY : coords.y - (valueBox.height / 2) + offsetY;
        valueBox.right = isTolBox ? coords.x + (valueBox.width / 2) - offsetX : coords.x + (valueBox.width / 2) + offsetX;
        valueBox.bottom = isTolBox ? coords.y + (valueBox.height / 2) - offsetY : coords.y + (valueBox.height / 2) + offsetY;

        valueBox.element.setAttribute("x", isTolBox ? coords.x - offsetX : coords.x + offsetX);
        valueBox.element.setAttribute("y", isTolBox ? coords.y - offsetY : coords.y + offsetY);
    }

    /**
     * Positions the value labels of the SVG.
     *
     * @param {*} self Reference to the control.
     * @param {number} arcStart Starting angle of the gauge arc in radiants.
     * @param {number} arcEnd Ending angle of the gauge arc in radiants.
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     * @param {Set} valueBoxes Set of value boxes
     * @param {Set} tolBoxes Set of tolerance boxes
     */
    function positionGaugeValueRangeLabels(self, arcStart, arcEnd, gaugeBoundingBox, valueBoxes, tolBoxes) {
        valueBoxes.forEach((box) => {
            doPositioning(self, arcStart, arcEnd, gaugeBoundingBox, box, false);
        });
        tolBoxes.forEach((box) => {
            doPositioning(self, arcStart, arcEnd, gaugeBoundingBox, box, true);
        });
    }

    /**
     * Fix collision of value boxes too close to another
     *
     * @param {Set} valueBoxes Set of value boxes
     * @param {valueBox} outerBox value box to check collision for
     * @param {number} outerIdx index of the value box
     */
    function fixCollision(valueBoxes, outerBox, outerIdx) {
        // Remove labels that collide. Prioritize the outermost labels.
        const boxText = (outer, inner) => (
            (
                (outer.left <= inner.left && inner.left <= outer.right) ||
                (outer.left <= inner.right && inner.right <= outer.right)
            ) && (
                (outer.top <= inner.top && inner.top <= outer.bottom) ||
                (outer.top <= inner.bottom && inner.bottom <= outer.bottom)
            )
        );

        if (outerBox.element.textContent === "") {
            // Element has already been hidden and can't collide anymore.
            return;
        }

        const collisions = valueBoxes.filter((innerBox, innerIdx) => innerIdx > outerIdx &&
            innerBox.element.textContent !== "" && (boxText(outerBox, innerBox) || boxText(innerBox, outerBox))
        );

        collisions.forEach((collidedBox) => {
            collidedBox.element.textContent = "";
            collidedBox.element.removeAttribute("x");
            collidedBox.element.removeAttribute("y");
        });
    }

    /**
     * Sorts and fixes all collisions of value and tolerance boxes
     *
     * @param {*} self Reference to the control.
     * @param {Set} valueBoxes Set of value boxes
     * @param {Set} tolBoxes Set of tolerance boxes
     */
    function sortAndFixGaugeValueRangeLabels(self, valueBoxes, tolBoxes) {
        // Sort the values inversely by their absolute distance from the
        // center value. By removing the first collision we find going by that
        // order, we preserve the outermost values.
        valueBoxes.sort(({ value: lhs }, { value: rhs }) => {
            const avg = (valueBoxes[0].value + valueBoxes[1].value) / 2;

            return Math.abs(rhs - avg) - Math.abs(lhs - avg);
        });
        tolBoxes.sort(({ value: lhs }, { value: rhs }) => {
            const avg = (tolBoxes[0].value + tolBoxes[1].value) / 2;

            return Math.abs(rhs - avg) - Math.abs(lhs - avg);
        });

        valueBoxes.forEach((outerBox, outerIdx) => {
            fixCollision(valueBoxes, outerBox, outerIdx);
        });
        tolBoxes.forEach((outerBox, outerIdx) => {
            fixCollision(tolBoxes, outerBox, outerIdx);
            if (shmi.hasClass(self.element, "iq-variant-01") && self.config["show-value"]) {
                const gvh = self.vars.cache.gaugeValueHolderInfo;
                if (((gvh.left + gvh.width >= outerBox.left || gvh.left >= outerBox.left) && (gvh.left + gvh.width <= outerBox.right || gvh.left <= outerBox.right)) &&
                    ((gvh.top + gvh.height >= outerBox.top || gvh.top >= outerBox.top) && (gvh.top + gvh.height <= outerBox.bottom || gvh.top <= outerBox.bottom))) {
                    outerBox.element.textContent = "";
                    outerBox.element.removeAttribute("x");
                    outerBox.element.removeAttribute("y");
                }
            }
        });
    }

    /**
     * Gets the intersection points with the arc circle
     *
     * @param {number} controlWidth Width of the gauge widget.
     * @param {number} textHeight Height of the value text.
     * @param {number} radius radius of the gauge arc.
     * @param {number} textYOffset Y translation of the value text.
     * @returns {object} x_left and x_right intersection points with the circle
     */
    function intersectionWithCircle(controlWidth, textHeight, radius, textYOffset) {
        const x_1 = -controlWidth / 2,
            x_2 = controlWidth / 2,
            y_1 = textYOffset + textHeight,
            y_2 = textYOffset + textHeight;

        const d_x = x_2 - x_1,
            d_y = y_2 - y_1,
            d_r = Math.sqrt(d_x ** 2 + d_y ** 2),
            D = x_1 * y_2 - x_2 * y_1;

        const temp = d_x * Math.sqrt(radius ** 2 * d_r **2 - D **2);
        const x_left = (D * d_y - temp) / (d_r ** 2),
            x_right = (D * d_y + temp) / (d_r ** 2);

        return {
            x_left, x_right
        };
    }

    /**
     * Gets the intersection points with the arc circle
     *
     * @param {number} controlWidth Width of the gauge widget.
     * @param {number} textHeight Height of the value text.
     * @param {number} radius radius of the gauge arc.
     * @param {number} textYOffset Y translation of the value text.
     * @param {number} arcAngleStart arc start angle
     * @param {number} arcAngleEnd arc end angle
     * @returns {object} x_left and x_right boundaries for the value text
     */
    function intersectionWithArc(controlWidth, textHeight, radius, textYOffset, arcAngleStart, arcAngleEnd) {
        let { x_left, x_right } = intersectionWithCircle(controlWidth, textHeight, radius, textYOffset);

        if (arcAngleEnd - arcAngleStart >= 2 * Math.PI) {
            return { x_left, x_right };
        }

        let flipped = false,
            thetaR = Math.acos(x_right / radius),
            thetaL = Math.acos(x_left / radius);

        if (textHeight + textYOffset < 0) { // we're looking for a negative angle
            thetaR = 2 * Math.PI - thetaR;
            thetaL = 2 * Math.PI - thetaL;
        }

        if (arcAngleStart < 0 && arcAngleEnd < 0) { // both arc angles are < 0, add 2 PI
            arcAngleStart += 2 * Math.PI;
            arcAngleEnd += 2 * Math.PI;
        } else if (arcAngleStart < 0) { // only arc start is < 0, add 2 PI and set flipped flag
            arcAngleStart += 2 * Math.PI;
            flipped = true;
        }

        if (!flipped) {
            if (thetaR < arcAngleStart || thetaR > arcAngleEnd) {
                x_right = Infinity;
            }
            if (thetaL < arcAngleStart || thetaL > arcAngleEnd) {
                x_left = -Infinity;
            }
        } else {
            if (thetaR <= arcAngleStart && thetaR >= arcAngleEnd) {
                x_right = Infinity;
            }
            if (thetaL <= arcAngleStart && thetaL >= arcAngleEnd) {
                x_left = -Infinity;
            }
        }

        if (isNaN(x_right)) {
            x_right = Infinity;
        }
        if (isNaN(x_left)) {
            x_left = -Infinity;
        }

        return { x_left, x_right };
    }

    /**
     * Apply the value styling for iq-variant-01 (value placed inside arc)
     *
     * @param {*} self Reference to the control.
     * @param {number} computedWidth Width of the gauge arc.
     * @param {number} computedHeight Height of the gauge arc.
     * @param {number} translateX X translation of the gauge arc.
     * @param {number} translateY Y translation of the gauge arc.
	 * @param {number} userTranslateY user translation of the gauge arc.
     * @param {BoundingBoxWithRadius} gaugeBoundingBox Bounding box of the arc.
     */
    function applyValueStyling(self, computedWidth, computedHeight, translateX, translateY, userTranslateY, gaugeBoundingBox) {
        const {
            cache: { gaugeWidth },
            elements: { gaugeValueHolder },
            arcAngleStart,
            arcAngleEnd
        } = self.vars;

        let posX = 0,
            posY = translateY + gaugeBoundingBox.centerY - gaugeValueHolder.clientHeight / 2,
            offsetY = 0;

        if (posY < 0) { // position outside of widget upper boundary
            offsetY = posY + userTranslateY;
            posY = 0;
        }
        if (posY + gaugeValueHolder.clientHeight > self.vars.cache.display.controlHeight) { // position outside of widget lower boundary
            offsetY = posY + gaugeValueHolder.clientHeight - self.vars.cache.display.controlHeight + userTranslateY;
            posY = self.vars.cache.display.controlHeight - gaugeValueHolder.clientHeight;
        }

        posY += userTranslateY;

        // get intersections with arc for upper and lower boundary of our text value
        const intersectionTop = intersectionWithArc(self.vars.cache.display.controlWidth, -gaugeValueHolder.clientHeight/2, gaugeBoundingBox.radius - gaugeWidth / 2 - VALUE_GAP, -offsetY + userTranslateY, arcAngleStart, arcAngleEnd),
            intersectionBottom = intersectionWithArc(self.vars.cache.display.controlWidth, gaugeValueHolder.clientHeight/2, gaugeBoundingBox.radius - gaugeWidth / 2 - VALUE_GAP, -offsetY + userTranslateY, arcAngleStart, arcAngleEnd);
        // filter the closest intersection to the center
        let x_left = Math.max(intersectionTop.x_left, intersectionBottom.x_left),
            x_right = Math.min(intersectionTop.x_right, intersectionBottom.x_right);

        if (x_left === -Infinity) { // should get as big as possible to the left
            x_left = -translateX - gaugeBoundingBox.centerX;
        }
        if (x_right === Infinity) { // should get as big as possible to the right
            x_right = -translateX - gaugeBoundingBox.centerX + self.vars.cache.display.controlWidth;
        }

        let valueWidth = x_right - x_left;
        posX = translateX + gaugeBoundingBox.centerX + x_left;

        if (posX < 0) { // position outside of widget to the left
            valueWidth += posX;
            posX = 0;
        }
        if (posX + valueWidth > self.vars.cache.display.controlWidth) { // position outside of widget to the right
            valueWidth -= posX + valueWidth - self.vars.cache.display.controlWidth;
            posX = self.vars.cache.display.controlWidth - valueWidth;
        }

        if (isNaN(x_left) || isNaN(x_right)) {
            gaugeValueHolder.style.visibility = "hidden";
        } else {
            gaugeValueHolder.style.visibility = "visible";
            gaugeValueHolder.style.left = `${posX + self.vars.cache.display.offsetLeft}px`;
            gaugeValueHolder.style.top = `${posY + self.vars.cache.display.offsetTop}px`;
            gaugeValueHolder.style.width = `${Math.max(valueWidth, 0)}px`;
        }

        self.vars.cache.gaugeValueHolderInfo = {
            left: posX - translateX,
            top: posY - translateY,
            width: Math.max(valueWidth, 0),
            height: gaugeValueHolder.clientHeight
        };
    }

    /**
     * Updates the components of the SVG that have been flagged.
     *
     * @param {*} self Reference to the control.
     * @param {number} width Width of the drawable area.
     * @param {number} height Height of the drawable area.
     * @param {number} arcStart Starting angle of the gauge arc in radiants.
     * @param {number} arcEnd Ending angle of the gauge arc in radiants.
     */
    function drawGaugeSvg(self, width, height, arcStart, arcEnd) {
        const { computeBoundingBoxForArc } = shmi.requires("iqGauge.gaugeHelpers"),
            {
                corners: { outer },
                cache: { gaugeWidth },
                elements: { gaugePathFill, gaugePathActualValueMin, gaugePathActualValueMax, gaugeContentContainer, gaugePathActualValueBar, gaugeValueHolder, labelValue },
                actValue: { value: actValue },
                valueSettings,
                drawFlags
            } = self.vars;

        // Check if there's something for us to update.
        if (Object.values(drawFlags).every((flag) => !flag)) {
            // Nothing to update.
            return;
        }

        const valueTicks = new Set,
            tolTicks = new Set;
        let valueRanges = null,
            boxInfo = null;

        if (drawFlags.background) {
            // Updates to the background may cause other things to change as
            // well. Just redraw everything.
            drawFlags.actValue = true;
            drawFlags.actLimits = true;
        }

        if (drawFlags.actLimits || drawFlags.tolLimits) {
            valueTicks.add(valueSettings.min);
            valueTicks.add(valueSettings.max);

            // Add set item limits (if set) to list of ticks.
            if (valueSettings.tolMin !== null) {
                tolTicks.add(valueSettings.tolMin);
            }

            if (valueSettings.tolMax !== null) {
                tolTicks.add(valueSettings.tolMax);
            }

            valueRanges = initGaugeValueRanges(self, valueTicks);

            if (self.config["value-labels"] > 1) {
                // Render labels for value range ticks.
                boxInfo = drawGaugeValueRangeLabels(self, valueTicks, tolTicks);
            }
        }

        const tickOffset = 2 * (TICK_LABEL_DISTANCE + TICK_LENGTH);

        self.vars.cache.boxWidth = boxInfo?.largestBox.width ?? self.vars.cache.boxWidth ?? 0;
        self.vars.cache.boxHeight = boxInfo?.largestBox.height ?? self.vars.cache.boxHeight ?? 0;

        const computedWidth = width - 2 * self.vars.cache.boxWidth - gaugeWidth - (self.config["value-labels"] ? tickOffset : 0),
            computedHeight = height - 2 * self.vars.cache.boxHeight - gaugeWidth - (self.config["value-labels"] ? tickOffset : 0);

        const gaugeBoundingBox = computeBoundingBoxForArc(computedWidth, computedHeight, arcStart, arcEnd),
            translateX = 0.5 * gaugeWidth + self.vars.cache.boxWidth + (self.config["value-labels"] ? tickOffset / 2 : 0),
            translateY = 0.5 * gaugeWidth + self.vars.cache.boxHeight + (self.config["value-labels"] ? tickOffset / 2 : 0);

        if (drawFlags.actLimits || drawFlags.tolLimits || drawFlags.background) {
            gaugeContentContainer.setAttribute("transform", `translate(${translateX} ${translateY})`);
        }

        if (computedWidth <= 0 || computedHeight <= 0) {
            shmi.addClass(self.vars.elements.svg, "invisible");
        } else {
            shmi.removeClass(self.vars.elements.svg, "invisible");
        }

        // Define a few helper binds so we don't have to throw so many arguments around all the time.

        /** @type {(value: number, radiusOffset?: number) => { x: number, y: number }} */
        const getPointAtValueHelper = getPointAtValue.bind(null, self, arcStart, arcEnd, gaugeBoundingBox),
            /** @type {(valueStart: number, valueEnd: number, radiusOffset?: number) => string} */
            renderArcHelper = renderRoundedArc.bind(null, self, arcStart, arcEnd, gaugeBoundingBox),
            clampValue = (value) => clamp(valueSettings.min, value, valueSettings.max);

        if (drawFlags.background) {
            const pStartMin = getPointAtValueHelper(clampValue(valueSettings.min), -gaugeWidth / 2),
                pStartMax = getPointAtValueHelper(clampValue(valueSettings.max), -gaugeWidth / 2),
                pEndMin = getPointAtValueHelper(clampValue(valueSettings.min), gaugeWidth / 2),
                pEndMax = getPointAtValueHelper(clampValue(valueSettings.max), gaugeWidth / 2);

            // Render the main background.
            gaugePathFill.setAttribute("d", renderArcHelper(valueSettings.min, valueSettings.max, outer));

            gaugePathActualValueMin.setAttribute("x1", pStartMin.x ? pStartMin.x : 0);
            gaugePathActualValueMin.setAttribute("y1", pStartMin.y ? pStartMin.y : 0);
            gaugePathActualValueMin.setAttribute("x2", pEndMin.x ? pEndMin.x : 0);
            gaugePathActualValueMin.setAttribute("y2", pEndMin.y ? pEndMin.y : 0);

            gaugePathActualValueMax.setAttribute("x1", pStartMax.x ? pStartMax.x : 0);
            gaugePathActualValueMax.setAttribute("y1", pStartMax.y ? pStartMax.y : 0);
            gaugePathActualValueMax.setAttribute("x2", pEndMax.x ? pEndMax.x : 0);
            gaugePathActualValueMax.setAttribute("y2", pEndMax.y ? pEndMax.y : 0);

            // arc is always drawn from min to max and clipped with the clipping mask
            gaugePathActualValueBar.setAttribute("d", renderArcHelper(valueSettings.min, valueSettings.max, outer));

            // Updates to the background may cause other things to change as
            // well. Just redraw everything.
            drawFlags.background = false;
            drawFlags.actValue = true;
            drawFlags.actLimits = true;

            if (self.config["show-value"]) {
                const userTranslateY = getStylePropertyValue(labelValue, "top");
                if (shmi.hasClass(self.element, "iq-variant-01")) {
                    applyValueStyling(self, computedWidth, computedHeight, translateX, translateY, userTranslateY, gaugeBoundingBox, pStartMin, pEndMin, pStartMax, pEndMax);
                } else {
                    gaugeValueHolder.style.top = userTranslateY + "px";
                }
            }
        }

        // Render main part of the gauge.
        if (drawFlags.actValue) {
            drawGaugeActValue(self, arcStart, arcEnd, gaugeBoundingBox);

            // Update warn state classes
            if (!self.config["disable-alarm-styling"] && self.config.item) {
                if (actValue < (valueSettings.prewarn.min ?? Number.NEGATIVE_INFINITY) || (valueSettings.prewarn.max ?? Number.POSITIVE_INFINITY) < actValue) {
                    if (!self.element.classList.contains("preWarn")) {
                        self.element.classList.add("preWarn");
                    }
                } else if (self.element.classList.contains("preWarn")) {
                    self.element.classList.remove("preWarn");
                }

                if (actValue < (valueSettings.warn.min ?? Number.NEGATIVE_INFINITY) || (valueSettings.warn.max ?? Number.POSITIVE_INFINITY) < actValue) {
                    if (!self.element.classList.contains("warn")) {
                        self.element.classList.add("warn");
                    }
                } else if (self.element.classList.contains("warn")) {
                    self.element.classList.remove("warn");
                }
            }

            if (self.config["value-labels"] > 0) {
                drawGaugeTolerances(self, arcStart, arcEnd, gaugeBoundingBox);
            }

            drawFlags.actValue = false;
        }

        // Render limit lines and ticks.
        if (drawFlags.actLimits || drawFlags.tolLimits) {
            // Render the value range indicators.
            if (self.config["value-labels"] > 0) {
                positionGaugeValueRanges(self, arcStart, arcEnd, gaugeBoundingBox, valueRanges);
                // Render value range ticks.
                drawGaugeValueRangeTicks(self, arcStart, arcEnd, gaugeBoundingBox, valueTicks);
                if (boxInfo) {
                    positionGaugeValueRangeLabels(self, arcStart, arcEnd, gaugeBoundingBox, boxInfo.valueBoxes, boxInfo.tolBoxes);
                    sortAndFixGaugeValueRangeLabels(self, boxInfo.valueBoxes, boxInfo.tolBoxes);
                }
            }

            drawFlags.actLimits = false;
            drawFlags.tolLimits = false;
        }
    }

    /**
     * Updates the svg's internal resolution to fit the element's dimensions.
     *
     * @param {*} self Reference to the control.
     */
    function updateSvgResolution(self) {
        const { clientWidth, clientHeight } = self.vars.elements.svg,
            { offsetTop, offsetLeft } = self.vars.elements.gaugeSvgSizeObserver,
            { svg: svgEl } = self.vars.elements;

        if (svgEl.width.baseVal.value !== svgEl.clientWidth) {
            svgEl.setAttribute("width", svgEl.clientWidth);
            self.vars.drawFlags.background = true;
        }

        if (svgEl.height.baseVal.value !== svgEl.clientHeight) {
            svgEl.setAttribute("height", svgEl.clientHeight);
            self.vars.drawFlags.background = true;
        }

        self.vars.cache.display = {
            controlWidth: clientWidth,
            controlHeight: clientHeight,
            offsetTop,
            offsetLeft
        };
    }

    /**
     * Checks if the control is ready for drawing.
     *
     * @param {*} self Reference to the control.
     * @returns {boolean} `true` if the control is ready, `false` else.
     */
    function isReady(self) {
        return Object.values(self.vars.elements).every((element) => !!element);
    }

    /**
     * Force a redraw of the gauge.
     *
     * @param {*} self Reference to the control.
     */
    function forceRedraw(self) {
        const { drawFlags } = self.vars;

        // Make sure the cache variables are present.
        checkCache(self);

        if (haveDisplayPropertiesChanged(self)) {
            updateSvgResolution(self);

            drawFlags.background = true;
        }

        drawGaugeSvg(self, self.vars.cache.display.controlWidth, self.vars.cache.display.controlHeight, self.vars.arcAngleStart, self.vars.arcAngleEnd);
    }

    /**
     * Schedules a control update on the next animation frame. If an update has
     * already been scheduled but not processed, will do nothing.
     *
     * @param {*} self Reference to the control.
     * @returns {?number} Id of the animation frame or `null` if an update
     *  has already been scheduled.
     */
    function startDrawing(self) {
        if (self.vars.animationFrameToken) {
            return null;
        }

        return shmi.raf((timestamp) => {
            if (!isReady(self)) {
                return;
            }

            const { drawFlags, elements: { labelValue } } = self.vars;

            // Process act/set value animations. If anything changed, set the
            // respective draw flags.
            drawFlags.actValue = self.vars.animators.actValue.tick(timestamp);

            // Make sure the cache variables are present.
            checkCache(self);

            if (labelValue && drawFlags.actValueLabel && self.config["show-value"]) {
                labelValue.textContent = self.imports.nv.formatOutput(self.vars.actValue.value ?? self.config["default-value"], self);
                drawFlags.actValueLabel = false;
            }

            if (haveDisplayPropertiesChanged(self)) {
                updateSvgResolution(self);

                drawFlags.background = true;
            }

            if (self.vars.elementIsInViewport) {
                drawGaugeSvg(self, self.vars.cache.display.controlWidth, self.vars.cache.display.controlHeight, self.vars.arcAngleStart, self.vars.arcAngleEnd);
            }
            // Set the token to `null`, as `startDrawing` checks for that.
            self.vars.animationFrameToken = null;
            self.vars.animationFrameToken = startDrawing(self);
        });
    }

    /**
     * Cancel the current animation frame. If no animation frame has been
     * requested, does nothing.
     *
     * @param {*} self Reference to the control.
     */
    function stopDrawing(self) {
        if (self.vars.animationFrameToken) {
            shmi.caf(self.vars.animationFrameToken);
            self.vars.animationFrameToken = null;
        }
    }

    /**
     * Adapts a DOM event listener to the visuals listener interface.
     *
     * @param {Element} domElement Element to attach the event listener to.
     * @param {string} eventName Name of the event to listen for.
     * @param {function} eventHandler Event handler function.
     *
     * @returns {{enable: function, disable: function}}
     */
    function makeDomEventListener(domElement, eventName, eventHandler) {
        return {
            enable: domElement.addEventListener.bind(domElement, eventName, eventHandler),
            disable: domElement.removeEventListener.bind(domElement, eventName, eventHandler)
        };
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            elements: {
                svg: null,
                gaugeContentContainer: null,
                gaugePathFill: null,
                gaugePathActualValueMin: null,
                gaugePathActualValueMax: null,
                gaugePathActualValueBar: null,
                gaugePathValueRangeNominal: null,
                gaugePathValueRangePrewarn: null,
                gaugePathValueRangeWarn: null,
                gaugePathValueRangeTicks: null,
                gaugePathValueRangeLabels: null,
                gaugePathTolRangeLabels: null,
                gaugeLineToleranceMin: null,
                gaugeLineToleranceMax: null,
                gaugeMask: null,
                gaugeMaskPath: null,
                gaugeSvgSizeObserver: null
            },
            elementIsInViewport: true,
            hasChanged: true,
            animationFrameToken: null,
            actValue: {
                value: null
            },
            arcAngleStart: 0,
            arcAngleEnd: 2 * Math.PI,
            valueSettings: {
                type: shmi.c("TYPE_FLOAT"),
                actValuePrecision: 1,
                min: 0,
                max: 100,
                prewarn: {
                    min: null,
                    max: null
                },
                warn: {
                    min: null,
                    max: null
                },
                tolMin: null,
                tolMax: null
            },
            cache: {
                gaugeWidth: null,
                boxWidth: 0,
                boxHeight: 0
            },
            corners: {
                outer: 12,
                inner: 4
            },
            animators: {
                actValue: null
            },
            drawFlags: {
                background: true,
                actLimits: true,
                actValue: true,
                tolLimits: true,
                actValueLabel: true
            },
            listeners: [],
            subscriptions: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            onInit: function onInit() {
                const { AnimationHelper } = shmi.requires("iqGauge.animationHelper"),
                    { applyIcon } = shmi.requires("visuals.tools.icons");
                /***********/
                /*** DOM ***/
                /***********/
                this.vars.elements.svg = shmi.getUiElement("gauge-svg", this.element);

                if (!this.vars.elements.svg) {
                    logger.error("Gauge SVG root not found in template.");
                    return;
                }

                // Get mandatory elements.
                this.vars.elements.gaugePathFill = shmi.getUiElement("gauge-fill", this.vars.elements.svg);
                this.vars.elements.gaugeContentContainer = shmi.getUiElement("content-container", this.vars.elements.svg);
                this.vars.elements.gaugePathActualValueMin = shmi.getUiElement("gauge-actual-value-min", this.vars.elements.svg);
                this.vars.elements.gaugePathActualValueMax = shmi.getUiElement("gauge-actual-value-max", this.vars.elements.svg);
                this.vars.elements.gaugePathActualValueBar = shmi.getUiElement("gauge-actual-value-bar", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeNominal = shmi.getUiElement("gauge-value-range-nominal", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangePrewarn = shmi.getUiElement("gauge-value-range-prewarn", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeWarn = shmi.getUiElement("gauge-value-range-warn", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeTicks = shmi.getUiElement("gauge-value-range-ticks", this.vars.elements.svg);
                this.vars.elements.gaugePathValueRangeLabels = shmi.getUiElement("gauge-value-range-labels", this.vars.elements.svg);
                this.vars.elements.gaugePathTolRangeLabels = shmi.getUiElement("gauge-tol-range-labels", this.vars.elements.svg);
                this.vars.elements.gaugeLineToleranceMin = shmi.getUiElement("gauge-tolerance-value-min", this.vars.elements.svg);
                this.vars.elements.gaugeLineToleranceMax = shmi.getUiElement("gauge-tolerance-value-max", this.vars.elements.svg);
                this.vars.elements.gaugeMask = shmi.getUiElement("gauge-mask", this.vars.elements.svg);
                this.vars.elements.gaugeMaskPath = shmi.getUiElement("gauge-mask-path", this.vars.elements.svg);
                this.vars.elements.gaugeSvgSizeObserver = shmi.getUiElement("gauge-svg-size-observer", this.element);
                this.vars.elements.gaugeValueHolder = shmi.getUiElement("gauge-value-holder", this.element);

                // Check if we're missing an element.
                const missingElements = Object.entries(this.vars.elements).filter(([name, element]) => !element).map(([name]) => name);
                if (missingElements.length > 0) {
                    logger.error(`Some elements are missing. Control unable to initialize:\n  - ${missingElements.join("\n  - ")}`);
                    return;
                }

                // Get optional elements.
                this.vars.elements.icon = shmi.getUiElement("icon", this.element);
                this.vars.elements.label = shmi.getUiElement("label", this.element);
                this.vars.elements.labelValue = shmi.getUiElement("value", this.element);
                this.vars.elements.labelUnit = shmi.getUiElement("unit", this.element);

                // Delete unavailable optional elements so that the ready check doesn't fail.
                Object.entries(this.vars.elements).forEach(([name, element]) => {
                    if (!element) {
                        delete this.vars.elements[name];
                    }
                });

                const gaugeMaskId = `gauge-mask-${getRandomId()}`;
                this.vars.elements.gaugeMask.setAttribute("id", gaugeMaskId);
                this.vars.elements.gaugePathActualValueBar.style.mask = `url(#${gaugeMaskId})`;

                // Resize svg's internal resolution to fit control.
                updateSvgResolution(this);

                // Map configured angles to radians
                let arcAngleStart = 0 + degToRad(this.config["arc-rotation"]);
                let arcAngleEnd = degToRad(this.config["arc-angle"]) + degToRad(this.config["arc-rotation"]);

                if (arcAngleStart > 0) {
                    // arcAngleStart is > 0. If we want to have a full circle and
                    // stay within [-2𝜋, 2𝜋], we need to subtract 2𝜋.
                    arcAngleStart -= 2 * Math.PI;
                    arcAngleEnd -= 2 * Math.PI;
                }
                // If the arc is larger than a circle, just make it a circle.
                if (arcAngleEnd - arcAngleStart > 2 * Math.PI) {
                    arcAngleEnd = arcAngleStart + 2 * Math.PI;
                }
                this.vars.arcAngleStart = arcAngleStart;
                this.vars.arcAngleEnd = arcAngleEnd;

                // Icon
                if (this.vars.elements.icon && this.config['icon-src']) {
                    applyIcon(this.vars.elements.icon, this.config["icon-src"], null, true);
                    shmi.addClass(this.vars.elements.icon, "inlined");
                } else if (this.vars.elements.icon && this.config['icon-class']) {
                    applyIcon(this.vars.elements.icon, null, this.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(this.element, "no-icon");
                }

                if (this.config["value-labels"] > 0) {
                    shmi.addClass(this.element, "has-ticks");
                }
                if (this.config["value-labels"] > 1) {
                    shmi.addClass(this.element, "has-labels");
                }
                if (!this.config["show-value"]) {
                    shmi.addClass(this.element, "no-value");
                }
                // Initialize label.
                setLabelImpl(this, this.config.label);

                // Initialize unit text.
                setUnitTextImpl(this, this.config['unit-text']);

                this.vars.valueSettings["decimal-delimiter"] = this.config["decimal-delimiter"] ? shmi.localize(this.config["decimal-delimiter"]) : ".";
                this.vars.cache.gaugeWidth = getStylePropertyValue(this.vars.elements.gaugePathFill, "width");
                this.vars.corners.outer = getStylePropertyValue(this.element, "--corner-radius-outer");
                this.vars.corners.inner = getStylePropertyValue(this.element, "--corner-radius-inner");

                if (this.vars.corners.outer > this.vars.cache.gaugeWidth / 2) {
                    this.vars.corners.outer = this.vars.cache.gaugeWidth / 2;
                }
                if (this.vars.corners.inner > this.vars.cache.gaugeWidth / 2) {
                    this.vars.corners.inner = this.vars.cache.gaugeWidth / 2;
                }
                if (this.vars.corners.inner > this.vars.corners.outer) {
                    this.vars.corners.inner = this.vars.corners.outer;
                }
                this.vars.animators.actValue = new AnimationHelper;

                // Setup the intersection observer. This observer is
                // responsible for pausing redraws while the widget is not in
                // the current viewport.
                this.vars.intersectionObserver = new IntersectionObserver((entries) => {
                    // If there's more than one entry, we're only really
                    // interested in the latest one.
                    if (entries.length > 1) {
                        entries.sort((lhs, rhs) => rhs.time - lhs.time);
                    }
                    this.vars.elementIsInViewport = entries[0].isIntersecting;
                });

                // Should anything change with our element, invalidate the
                // cache. This will cause everything to be recomputed and
                // redrawn on the next frame.
                this.vars.mutationObserver = new MutationObserver(() => {
                    this.vars.cache = null;
                });
                this.vars.resizeObserver = new ResizeObserver(() => {
                    this.vars.cache = null;
                    forceRedraw(this);
                });

                // Register resize handler. Workaround for detecting style
                // changes. Clears cache on every `resize` event, causing
                // styles to be reevaluated.
                this.vars.listeners.push(makeDomEventListener(window, "resize", () => {
                    // Clearing the cache causes `haveDisplayPropertiesChanged` to
                    // return `true`, which in turn causes the CSS to be reevaluated.
                    this.vars.cache = null;
                }));
            },
            onEnable: function onEnable() {
                if (!isReady(this)) {
                    return;
                }

                // Attach the observers.
                this.vars.intersectionObserver?.observe(this.vars.elements.svg);
                this.vars.mutationObserver?.observe(this.element, { attributes: true, attributeFilter: ['style', 'class'] });
                this.vars.resizeObserver?.observe(this.vars.elements.gaugeSvgSizeObserver);

                checkCache(this);
                startDrawing(this);

                // Subscribe act item (if configured).
                if (this.config["item"]) {
                    this.vars.subscriptions.push(this.imports.im.subscribeItem(this.config["item"], {
                        setValue: (value) => {
                            this.vars.actValue.value = value;
                            this.vars.animators.actValue.set(value, parseInt(this.config["animation-duration"]) ?? 0);
                            this.vars.drawFlags.actValueLabel = true;
                        },
                        setProperties: (min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision, tolmin, tolmax) => {
                            // Make sure the values are well behaved.
                            min = (this.config["auto-min"] ? min : null) ?? this.config["min"] ?? 0;
                            max = (this.config["auto-max"] ? max : null) ?? this.config["max"] ?? 100;
                            prewarnmin = prewarnmin ?? null;
                            prewarnmax = prewarnmax ?? null;
                            warnmin = warnmin ?? null;
                            warnmax = warnmax ?? null;
                            tolmin = tolmin ?? null;
                            tolmax = tolmax ?? null;
                            type = (this.config["auto-type"] ? type : null) ?? this.config["type"] ?? shmi.c("TYPE_FLOAT");
                            precision = (this.config["auto-precision"] ? precision : null) ?? this.config["precision"] ?? 1;

                            // Check if anything has changed at all. If so, we
                            // need to redraw the act value limits.
                            const limitsHaveChanged =
                                this.vars.valueSettings.min !== min ||
                                this.vars.valueSettings.max !== max ||
                                this.vars.valueSettings.prewarn.min !== prewarnmin ||
                                this.vars.valueSettings.prewarn.max !== prewarnmax ||
                                this.vars.valueSettings.warn.min !== warnmin ||
                                this.vars.valueSettings.warn.max !== warnmax ||
                                this.vars.valueSettings.tolMin !== tolmin ||
                                this.vars.valueSettings.tolMax !== tolmax ||
                                this.vars.valueSettings.actValuePrecision !== precision;

                            this.vars.valueSettings.min = min;
                            this.vars.valueSettings.max = max;
                            this.vars.valueSettings.type = type;
                            this.vars.valueSettings.prewarn.min = prewarnmin;
                            this.vars.valueSettings.prewarn.max = prewarnmax;
                            this.vars.valueSettings.warn.min = warnmin;
                            this.vars.valueSettings.warn.max = warnmax;
                            this.vars.valueSettings.tolMin = tolmin;
                            this.vars.valueSettings.tolMax = tolmax;
                            this.vars.valueSettings.actValuePrecision = precision;
                            this.vars.valueSettings.precision = precision; // for nv formatOutput

                            // Make sure the relevant parts of the gauge are
                            // updated.
                            if (limitsHaveChanged) {
                                this.vars.drawFlags.background = true;
                                this.vars.drawFlags.actLimits = true;
                                this.vars.drawFlags.actValue = true;
                            }
                            this.vars.drawFlags.tolLimits = this.vars.drawFlags.tolLimits ||
                                this.vars.valueSettings.tolMin !== min ||
                                this.vars.valueSettings.tolMax !== max;
                        },
                        setLabel: this.setLabel.bind(this),
                        setUnitText: this.setUnitText.bind(this)
                    }));
                }

                this.vars.listeners.forEach((l) => l.enable());
            },
            onDisable: function onDisable() {
                if (!isReady(this)) {
                    return;
                }

                this.vars.listeners.forEach((l) => l.disable());

                stopDrawing(this);
                this.vars.resizeObserver?.disconnect(this.vars.elements.gaugeSvgSizeObserver);
                this.vars.intersectionObserver?.disconnect(this.vars.elements.svg);
                this.vars.mutationObserver?.disconnect(this.element);
                this.vars.subscriptions.forEach((s) => s.unlisten());
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function onLock() {
                shmi.addClass(this.element, 'locked');
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function onUnlock() {
                shmi.removeClass(this.element, 'locked');
            },

            getValue: function() {
                return this.vars.actValue.value;
            },

            setUnitText: function(unitText) {
                const self = this;

                if (self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals iq-recipe-list control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-recipe-list",
 *     "name": null,
 *     "template": "default/iq-recipe-list",
 *     "recipe-template-id": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-recipe-list", // control name in camel-case
        uiType = "iq-recipe-list", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": className,
        "name": null,
        "template": "default/iq-recipe-list.iq-variant-01",
        "label": uiType,
        "recipe-template-id": null,
        "columnConfig": {
            "col1": {
                "fields": ["id"],
                "column-width": "15%",
                "hidden": false,
                "minMediaQuery": null
            },
            "col2": {
                "fields": ["name"],
                "column-width": "40%",
                "hidden": false,
                "minMediaQuery": null
            }
        }
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function eventProxy(self, event) {
        self.fire(event.type, event.detail);
    }

    function doSetValue(val, self) {
        const selObj = {
            selRowIndex: [],
            selRows: [],
            type: -1
        };

        for (let i = 0; i < self.vars.dataGrid.getRowCount(); i++) {
            const rowData = self.vars.dataGrid.getRowData(i);
            if (rowData && rowData[0].value === parseInt(val)) {
                selObj.selRowIndex = [[rowData[0].value]];
                selObj.selRows = [i];
                selObj.type = 2;
                break;
            }
        }
        self.vars.ct.setSelectedRows(selObj);
    }

    function checkIfSetValue(val, self) {
        if (self.vars.dataGrid && self.vars.ct) {
            if (self.vars.dataGrid.tasksRunning > 0) {
                const tok = (shmi.listen("datagrid-ready", function() {
                    tok.unlisten();
                    doSetValue(val, self);
                }.bind(val, self)));
            } else {
                doSetValue(val, self);
            }
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domChild: null,
            dataGridId: null,
            dataGrid: null,
            dataGridSubscriptionId: null,
            ct: null,
            eventProxy: null,
            tokens: [],
            recipeTableConfig: {
                "label": "${recipe-list.title}",
                "table": "",
                "name": "",
                "class-name": "complex-table2",
                "field-datagrid-col-map": {
                    "id": 0,
                    "name": 1
                },
                "select-mode": "SINGLE",
                "default-field-control-map": {
                    "id": {
                        "ui-type": "iq-label",
                        "config": {
                            "class-name": "iq-label",
                            "options": [],
                            "pattern": "<%= VALUE %>",
                            "value-as-tooltip": true
                        }
                    },
                    "name": {
                        "ui-type": "iq-label",
                        "config": {
                            "class-name": "iq-label",
                            "options": [],
                            "pattern": null,
                            "value-as-tooltip": true
                        }
                    }
                },
                "default-field-headers": {
                    "id": "${recipe-list.table-header.id}",
                    "name": "${recipe-list.table-header.name}"
                },
                "filters": [],
                "default-layout": {
                    "class-name": "layout-std",
                    "column-org": {
                        "col1": {
                            "fields": ["id"],
                            "column-width": "15%"
                        },
                        "col2": {
                            "fields": ["name"],
                            "column-width": "85%"
                        }
                    },
                    "line-height": "var(--table-row-height)"
                },
                "sortable-fields": [
                    "name",
                    "id"
                ],
                "quicksearch": {
                    "enable": true,
                    "remember": true,
                    "fields": ["name"]
                },
                "text-mode": "SINGLELINE",
                "show-nof-rows": true,
                "v-scroll-options": ["SCROLLBAR", "V_SWIPE"]
            }
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "select"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                if (typeof (this.config["recipe-template-id"]) !== "number") {
                    this.element.textContent = "Invalid or no recipe template id given. This control will not work. Please select a valid recipe template id.";
                    fLog("Invalid or no recipe template id given:", this.config["recipe-template-id"]);
                    return;
                }

                this.vars.domChild = shmi.getUiElement("table-container", this.element);
                if (this.vars.domChild === null) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                this.imports.dgm.grids[this.vars.dataGridId] = this.vars.dataGrid = new shmi.visuals.core.DataGridRecipe(this.vars.dataGridId, this.config["recipe-template-id"]);
                this.vars.dataGrid.init();
                this.vars.dataGrid.sort(1, "ASC");

                this.rebuildTable();
            },
            rebuildTable: function() {
                const tableConfig = shmi.cloneObject(this.vars.recipeTableConfig);

                tableConfig.table = this.vars.dataGridId;
                tableConfig.name = this.vars.dataGridId;
                tableConfig["default-layout"]["column-org"] = this.config.columnConfig;
                this.vars.dataGrid.sort(1, "ASC");

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }

                if (this.vars.ct) shmi.deleteControl(this.vars.ct);

                // Workaround for shmi.getControlByElement called inside
                // shmi.createControl throwing an exception if `data-ui`
                // is set to something that's not the type of a widget.
                this.vars.domChild.removeAttribute("data-ui");
                this.vars.ct = shmi.createControl("complex-table2", this.vars.domChild, tableConfig, null, "from");
                if (this.vars.ct === null) {
                    this.element.textContent = "Unable to create table control for recipes. This control will not work.";
                    fLog("Unable to create table control for recipes.");
                } else {
                    this.vars.eventProxy = this.vars.ct.listen("select", eventProxy.bind(null, this));
                }
            },
            /**
             * Returns the contents of the table selection
             *
             * @returns {object} selected recipe info
             */
            getValue: function() {
                if (this.vars.ct) {
                    const selection = this.vars.ct.getSelectionRowData(this.vars.ct.getSelectedRows());
                    if (selection.length > 0) {
                        return {
                            name: selection[0][1].value,
                            recipe_id: isNaN(selection[0][0].value) ? null : selection[0][0].value,
                            template_id: this.config["recipe-template-id"],
                            values_set: (selection[0][3].value === 1)
                        };
                    }
                }
                return null;
            },
            /**
             * Sets the value of the table selection.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                const self = this;
                checkIfSetValue(val, self);
            },
            refreshGrid: function() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.vars.ct) {
                    this.vars.ct.enable();
                    if (!this.vars.eventProxy) {
                        this.vars.eventProxy = this.vars.ct.listen("select", eventProxy.bind(null, this));
                    }
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                if (this.vars.ct) {
                    this.vars.ct.disable();
                }

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }
            },
            onDelete: function() {
                if (this.vars.ct) {
                    shmi.deleteControl(this.vars.ct);
                }

                if (this.vars.dataGrid) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
                this.vars.tokens.forEach((tok) => {
                    tok.unlisten();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                if (this.vars.ct) {
                    this.vars.ct.lock();
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.ct) {
                    this.vars.ct.unlock();
                }
            },
            getSelectedRecipe: function() {
                if (this.vars.ct) {
                    return this.vars.ct.getSelectionRowData(this.vars.ct.getSelectedRows());
                }
                return null;
            },
            getTemplate: function() {
                return this.config["recipe-template-id"];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * IQ Responsive Menu
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-responsive-menu",
 *     "name": null,
 *     "template": "default/iq-responsive-menu"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 0.2
 */
(function() {
    'use strict';

    //variables for reference in control definition
    const className = "IqResponsiveMenu", //control name in camel-case
        uiType = "iq-responsive-menu", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": `default/${uiType}`,
        "menu": [],
        "mobile-layouts": [],
        "overlay-style": {
            "background": "transparent",
            "zIndex": 1
        },
        "show-entry-state-on-section": true
    };

    /* Example for config.menu structure */
    /**
    const exampleMenu = [
        {
            "label": "My Category",
            "items": [
                {
                    "label": "My Sub-Category",
                    "items": [
                        {
                            "label": "Menu Entry",
                            "action": [
                                {
                                    "name": "test-action",
                                    "params": [1, 2, 3]
                                }
                            ],
                            "icon-src": "pics/system/icons/debug_icon.png" // image icon
                        },
                        {
                            "label": "Menu Entry",
                            "action": [
                                {
                                    "name": "test-action",
                                    "params": [1, 2, 3]
                                }
                            ],
                            "icon-class": "icon icon-add" // icon font class
                        }
                    ]
                },
                {
                    "label": "Menu Entry",
                    "action": [
                        {
                            "name": "test-action",
                            "params": [1, 2, 3]
                        },
                        {
                            "name": "test-action",
                            "params": [4, 5, 6]
                        }
                    ]
                }
            ]
        },
        {
            "label": "Menu Entry",
            "action": [
                {
                    "name": "test-action",
                    "params": [1, 2, 3]
                }
            ]
        },
        {
            "label": "Menu Entry",
            "action": [
                {
                    "name": "test-action",
                    "params": [4, 5, 6]
                }
            ]
        }
    ];
    */

    //setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    const DEFAULT_ICON = "pics/system/icons/placeholder.svg",
        SECTION_ICON = "pics/system/controls/iq-responsive-menu/expand_more.svg",
        SECTION_ICON_SECONDARY = "pics/system/controls/iq-responsive-menu/arrow_right.svg";

    //declare private functions - START
    /**
     * Creates a compound listener acting both as touch- and mouse-listener.
     * Creates a touch- and mouse-listener internally and enables/disables them
     * both at the same time.
     *
     * @param {HTMLElement} element Element to attach the listener to.
     * @param {object} handlers Object specifying the corresponding handlers
     *  called by the touch- and mouse-listeners.
     * @returns {object} Object with a listener-like interface.
     */
    function createListener(element, handlers) {
        const io = shmi.requires("visuals.io"),
            ml = new io.MouseListener(element, handlers),
            tl = new io.TouchListener(element, handlers);
        return {
            enable: function() {
                ml.enable();
                tl.enable();
            },
            disable: function() {
                ml.disable();
                tl.disable();
            }
        };
    }

    /**
     * Creates a new html element with css classes and data-ui set.
     *
     * @param {string|string[]} cssClasses CSS class or classes to assign to
     *  the new element.
     * @param {?string} setUiType ui type to set.
     * @param {string} [elementType] Type of the html element. Defaults to
     *  "DIV".
     * @returns {HTMLElement}
     */
    function makeElement(cssClasses, setUiType, elementType = "DIV") {
        const elem = document.createElement(elementType || "DIV");
        if (typeof cssClasses === "string") {
            shmi.addClass(elem, cssClasses);
        } else if (Array.isArray(cssClasses)) {
            cssClasses.forEach(function(cc) {
                if (typeof cc === "string") {
                    shmi.addClass(elem, cc);
                }
            });
        }
        if (typeof setUiType === "string") {
            elem.setAttribute("data-ui", setUiType);
        }
        return elem;
    }

    /**
     * Marks the given layer as "selected". Successive layers are unmarked as
     * "selected".
     *
     * @param {*} self Reference to the widget.
     * @param {?HTMLElement} element Element to assign the "selected" CSS class
     *  to. If set to `null`, in addition to the successive layers being
     *  unmarked as "selected", the given one is unmarked as well.
     * @param {number} layer Layer index
     */
    function setSelected(self, element, layer) {
        if (self.vars.selected[layer] !== element) {
            if (self.vars.selected[layer] !== null) {
                shmi.removeClass(self.vars.selected[layer], "selected");
            }
            self.vars.selected[layer] = element;
            if (element !== null) {
                shmi.addClass(element, "selected");
            }
        }

        self.vars.selected.forEach(function(e, i) {
            if (e && (i > layer)) {
                shmi.removeClass(e, "selected");
                self.vars.selected[i] = null;
            }
        });
    }

    /**
     * Creates a menu section according to the provided configuration. Child
     * elements are created recursively.
     *
     * @param {*} self Reference to the widget.
     * @param {object} sectionConfig Configuration of the menu section.
     * @param {number} [layer] Number of the layer the section is located on.
     * @returns {object} Descriptor of the new menu section.
     */
    function makeMenuSection(self, sectionConfig, layer = 0) {
        const { applyIcon } = shmi.requires("visuals.tools.icons");
        const baseElement = makeElement("iqel-menu-group iqel-menu-element", "menu-group", "LI"),
            groupData = makeElement("iqel-group-data"),
            stateLayer = makeElement("iqel-state-layer"),
            iconWrapper = makeElement("iqel-icon", "icon"),
            label = makeElement("iqel-menu-group-label iqel-label", "menu-group-label"),
            triangle = makeElement("iqel-triangle"),
            arrow = makeElement("iqel-arrow"),
            childList = makeElement(`iqel-menu-layer-${layer + 1} iqel-menu-layer-${layer}-${layer + 1} iqel-menu-layer-${layer + 1}-${layer + 2} iqel-menu-background accent-border`, `menu-layer-${layer + 1}`, "UL");

        const children = sectionConfig.items.map((child) => makeMenuElement(self, child, layer + 1)).filter((child) => !!child);

        // Piece together DOM elements
        groupData.appendChild(stateLayer);
        stateLayer.appendChild(iconWrapper);
        stateLayer.appendChild(label);
        stateLayer.appendChild(triangle);
        stateLayer.appendChild(arrow);

        children.forEach(({ elements: { base } }) => childList.appendChild(base));

        baseElement.appendChild(groupData);
        baseElement.appendChild(childList);

        const listener = createListener(groupData, {
            onClick: function onClick() {
                const crumb = self.vars.crumbs[`layer${layer}`];
                if (shmi.hasClass(baseElement, "selected")) {
                    setLayer(self, layer);
                    setSelected(self, null, layer);
                    if (layer === 0 && !self.vars.mobile) {
                        removeOverlay(self);
                    }
                } else {
                    setLayer(self, layer + 1);
                    setSelected(self, baseElement, layer);
                    crumb.textContent = shmi.localize(sectionConfig.label);
                    if (layer === 0 && !self.vars.mobile) {
                        createOverlay(self);
                    }
                }
            }
        });

        // Setup DOM elements
        label.textContent = shmi.localize(sectionConfig.label);
        applyIcon(iconWrapper, sectionConfig["icon-src"] ?? DEFAULT_ICON, null, sectionConfig["inline-svg"] ?? true);
        applyIcon(triangle, SECTION_ICON, null, true);
        applyIcon(arrow, SECTION_ICON_SECONDARY, null, true);

        if (sectionConfig["icon-mode"] === "no-icon") {
            baseElement.classList.add("no-icon");
        } else if (sectionConfig["icon-mode"] === "hidden") {
            baseElement.classList.add("hidden-icon");
        }

        if (layer === 0) {
            baseElement.classList.add("accent-border");
        }

        const sectionEntry = {
            type: "section",
            config: {
                label: sectionConfig["label"] || null,
                iconSrc: sectionConfig["icon-src"] || null,
                iconMode: sectionConfig["icon-mode"] || "show",
                access: shmi.cloneObject(sectionConfig.access)
            },
            listeners: [listener],
            elements: {
                base: baseElement,
                label: label,
                childList: childList
            },
            children: children
        };

        children.forEach((child) => {
            child.parent = sectionEntry;
        });

        return sectionEntry;
    }

    /**
     * Creates a menu entry according to the provided configuration.
     *
     * @param {*} self Reference to the widget.
     * @param {object} entryConfig Configuration of the menu entry.
     * @param {number} [layer] Number of the layer the entry is located on.
     * @returns {object} Descriptor of the new menu entry.
     */
    function makeMenuEntry(self, entryConfig, layer = 0) {
        const { applyIcon } = shmi.requires("visuals.tools.icons");
        const baseElement = makeElement("iqel-menu-item iqel-menu-element", "menu-item", "LI"),
            stateLayer = makeElement("iqel-state-layer"),
            iconWrapper = makeElement("iqel-icon", "icon"),
            label = makeElement("iqel-menu-item-label iqel-label", "menu-item-label"),
            { UiAction } = shmi.requires("visuals.core"),
            action = entryConfig.action ? new UiAction(entryConfig.action) : null,
            navigationTarget = entryConfig.navigationTarget;

        // Piece together DOM elements
        baseElement.appendChild(stateLayer);
        stateLayer.appendChild(iconWrapper);
        stateLayer.appendChild(label);

        // Setup DOM elements
        label.textContent = shmi.localize(entryConfig.label);
        applyIcon(iconWrapper, entryConfig["icon-src"] ?? DEFAULT_ICON, null, entryConfig["inline-svg"] ?? true);

        if (entryConfig["icon-mode"] === "no-icon") {
            baseElement.classList.add("no-icon");
        } else if (entryConfig["icon-mode"] === "hidden") {
            baseElement.classList.add("hidden-icon");
        }

        if (layer === 0) {
            baseElement.classList.add("accent-border");
        }

        const listener = createListener(baseElement, {
            onClick: function onClick() {
                setLayer(self, -1);
                setSelected(self, baseElement, layer);
                removeOverlay(self);
                if (action) {
                    action.execute(self);
                }

                if (navigationTarget) {
                    shmi.visuals.tools.navigation.navigate(navigationTarget);
                }
            }
        });

        return {
            type: "entry",
            config: {
                label: entryConfig["label"] || null,
                iconSrc: entryConfig["icon-src"] || null,
                iconMode: entryConfig["icon-mode"] || "show",
                access: shmi.cloneObject(entryConfig.access),
                action: shmi.cloneObject(entryConfig.action),
                navigationTarget: entryConfig.navigationTarget ?? null
            },
            listeners: [listener],
            elements: {
                base: baseElement,
                label: label
            }
        };
    }

    /**
     * Creates a menu separator according to the provided configuration.
     *
     * @param {*} self Reference to the widget.
     * @param {object} separatorConfig Configuration of the menu separator.
     * @param {number} [layer] Number of the layer the separator is located on.
     * @returns {object} Descriptor of the new menu separator.
     */
    function makeMenuSeparator(self, separatorConfig, layer = 0) {
        const baseElement = makeElement("iqel-menu-item iqel-separator", "menu-item", "LI"),
            label = makeElement("iqel-menu-item-label", "menu-item-label");

        // Piece together DOM elements
        baseElement.appendChild(label);

        // Setup DOM elements
        label.textContent = shmi.localize(separatorConfig.label);

        if (separatorConfig["show-stroke"]) {
            baseElement.classList.add("show-stroke");
        }

        return {
            type: "separator",
            config: {
                label: separatorConfig["label"] || null,
                showStroke: separatorConfig["show-stroke"] || false
            },
            listeners: [],
            elements: {
                base: baseElement,
                label: label
            }
        };
    }

    /**
     * Creates a menu element based on the element type in the provided
     * configuration. If the configuration doesn't include a type, elements
     * with children are assumed to be sections while everything else is
     * assumed to be entries.
     *
     * @param {*} self Reference to the widget.
     * @param {object} config Configuration of the menu element.
     * @param {number} layer Number of the layer the menu element is located
     *  on.
     * @returns {object} Descriptor of the new menu element.
     */
    function makeMenuElement(self, config, layer) {
        const elementType = config.type || (Array.isArray(config.items) ? "group" : "item");

        switch (elementType) {
        case "group":
            if (layer > 1) {
                console.warn(`[${className}] Creating sections for layers >1 not supported.`);
            }

            return makeMenuSection(self, config, layer);

        case "item":
            return makeMenuEntry(self, config, layer);

        case "separator":
            return makeMenuSeparator(self, config, layer);

        default:
            console.warn(`[${className}] Unknown type for menu element ${elementType}`);
            return null;
        }
    }

    /**
     * Creates the menu root.
     *
     * @param {*} self Reference to the widget.
     * @param {object[]} menu Menu entries for layer 0
     * @returns {object} Descriptor of the new menu.
     */
    function makeMenu(self, menu) {
        const menuBase = shmi.getUiElement("menu-layer-0", self.element),
            burgerButton = shmi.getUiElement("burger-button", self.element);

        const children = menu.map((menuElement) => makeMenuElement(self, menuElement, 0)).filter((element) => !!element);

        // Piece together DOM elements
        children.forEach(({ elements: { base } }) => menuBase.appendChild(base));

        // Setup DOM elements
        const listener = createListener(burgerButton, {
            onClick: function onClick() {
                if (self.vars.activeLayer >= 0 && !self.vars.mobile) {
                    setLayer(self, -1);
                    removeOverlay(self);
                } else {
                    setLayer(self, 0);
                    createOverlay(self);
                }
            }
        });

        const baseEntry = {
            type: "menu",
            config: {},
            listeners: [listener],
            children: children
        };

        children.forEach((child) => {
            child.parent = baseEntry;
        });

        return baseEntry;
    }

    /**
     * Returns an array of all listeners of a menu.
     *
     * @param {object} menu Descriptor of a menu or menu element.
     * @returns {object[]} Array of listeners.
     */
    function collectListeners(menu) {
        if (!Array.isArray(menu.children)) {
            return menu.listeners || [];
        }

        return menu.children.reduce((combined, current) => combined.concat(collectListeners(current)), menu.listeners);
    }

    /**
     * Returns an array of all access condition data of a menu.
     *
     * @param {*} menu Descriptor of a menu or menu element.
     * @returns {object[]} Array of access condition data.
     */
    function collectAccessData(menu) {
        const accessData = [];

        if (menu.config.access && menu.config.access.condition && ["hide", "lock"].includes(menu.config.access.type)) {
            accessData.push({
                base: menu.elements.base,
                type: menu.config.access.type,
                condition: menu.config.access.condition,
                listeners: menu.listeners.slice(),
                menu
            });
        }

        if (!Array.isArray(menu.children)) {
            return accessData;
        }

        return menu.children.reduce((combined, current) => combined.concat(collectAccessData(current)), accessData);
    }

    /**
     * Sets the layer-active css class for the corresponding layer.
     * If `layer` is set to -1, all selections are cleared.
     *
     * @param {*} self Reference to the widget.
     * @param {number} layer Layer index
     */
    function setLayer(self, layer) {
        if (layer !== self.vars.activeLayer) {
            if (self.vars.activeLayer >= 0) {
                shmi.removeClass(self.element, `layer-${self.vars.activeLayer}-active`);
            }
            self.vars.activeLayer = layer;
            if (layer >= 0) {
                shmi.addClass(self.element, `layer-${self.vars.activeLayer}-active`);
            }
        }

        if (layer === -1) {
            self.vars.selected.forEach(function(e, idx) {
                if (e !== null) {
                    shmi.removeClass(e, "selected");
                    self.vars.selected[idx] = null;
                }
            });
        }
    }

    /**
     * Returns a condition callback for the given access data reference.
     *
     * @param {HTMLElement} param0.base HTML element to add/remove the css
     *  modifiers to/from.
     * @param {object[]} param0.listeners Listeners to enable/disable.
     * @param {string} cssClass CSS modifier to add/remove.
     * @returns {function}
     */
    function makeAccessConditionCallback({ base, listeners }, cssClass) {
        let lastState = false,
            lastActionState = false,
            disabled = false;

        function performAlways(isActive) {
            if (!isActive) {
                shmi.addClass(base, cssClass);
            } else {
                shmi.removeClass(base, cssClass);
            }
        }

        function performAction(isActive) {
            lastActionState = isActive;
            if (!isActive) {
                listeners.forEach((l) => l.disable());
            } else {
                listeners.forEach((l) => l.enable());
            }
        }

        function accessConditionCallback(isActive) {
            lastState = isActive;
            performAlways(isActive);
            if (!disabled) {
                performAction(lastState);
            }
        }

        accessConditionCallback.disable = () => {
            disabled = true;
            if (lastActionState) {
                performAction(false);
            }
        };
        accessConditionCallback.enable = () => {
            disabled = false;
            if (lastState !== lastActionState) {
                performAction(lastState);
            }
        };

        return accessConditionCallback;
    }

    function createOverlay(self) {
        if (self.vars.overlay) {
            removeOverlay(self);
        }

        const overlay = {
                element: document.createElement("DIV"),
                listener: null
            },
            handler = { onClick: null },
            elem = overlay.element,
            s = elem.style;
        s.position = "fixed";
        s.top = 0;
        s.bottom = 0;
        s.left = 0;
        s.right = 0;
        s.background = self.config["overlay-style"].background;
        s.zIndex = self.config["overlay-style"].zIndex;

        document.body.insertBefore(overlay.element, document.body.firstChild);

        handler.onClick = function() {
            if (self.vars.activeLayer >= 0) {
                setLayer(self, -1);
                removeOverlay(self);
            }
        };
        overlay.listener = createListener(overlay.element, handler);
        overlay.listener.enable();
        self.vars.overlay = overlay;
        document.addEventListener("click", self.vars.documentListener, { capture: true });
    }

    function removeOverlay(self) {
        if (self.vars.overlay) {
            self.vars.overlay.element.parentNode.removeChild(self.vars.overlay.element);
            self.vars.overlay.listener.disable();
            self.vars.overlay = null;
            document.removeEventListener("click", self.vars.documentListener, { capture: true });
        }
    }

    /**
     * handle events on document element to register clicks not captured by overlay
     *
     * @param {object} self instance reference
     * @param {object} event click event
     */
    function documentListener(self, event) {
        if ((self.vars.activeLayer >= 0) && event.target && !shmi.testParentChild(self.element, event.target)) {
            setLayer(self, -1);
            removeOverlay(self);
        }
    }

    /**
     * Toggles navigation-active class for all parent elements up to instance reference
     * Checks for already active entries and find parent element
     *
     * @param {Object} self instance reference
     * @param {Object} element current element
     * @param {boolean} isActive add or remove class
     */
    function updateSectionActiveState(self, element, isActive) {
        if (!self.config["show-entry-state-on-section"]) {
            return;
        }

        element = element.parent;
        while (element && element.type !== "menu") {
            if (element.type === "section") {
                if (isActive) {
                    if (!element.elements.base.classList.contains("navigation-active")) {
                        shmi.addClass(element.elements.base, "navigation-active");
                    }
                } else {
                    const hasOtherActiveEntries = element.children?.some((child) =>
                        child.type === "entry" && child.config?.navigationTarget && shmi.visuals.tools.navigation.isNavigationActive(child.config.navigationTarget)
                    );
                    if (!hasOtherActiveEntries) {
                        shmi.removeClass(element.elements.base, "navigation-active");
                    }
                }
            }
            element = element.parent;
        }
    }

    /**
     * Updates navigation state by adding or removing the "navigation-active" class
     * for entries and their parent sections based on the navigation targets
     *
     * @param {Object} self instance reference
     * @param {boolean} disable disables navigation state updates
     * @param {Array<Object>} currentElement current list of elements
     */
    function updateNavigationState(self, disable, currentElement) {
        if (!currentElement) {
            currentElement = self.vars.menu?.children ?? [];
        }

        currentElement.forEach((element) => {
            if (element.type === "entry" && element.config.navigationTarget) {
                if (!disable) {
                    try {
                        self.vars.tokens.push(shmi.visuals.tools.navigation.listenNavigationState(element.config.navigationTarget, (evt) => {
                            const isActive = evt.detail.active;
                            if (isActive) {
                                shmi.addClass(element.elements.base, "navigation-active");
                            } else {
                                shmi.removeClass(element.elements.base, "navigation-active");
                            }
                            updateSectionActiveState(self, element, isActive);
                        }));
                    } catch (err) {
                        console.error("Error creating navigation state listener: ", err);
                    }

                    const isActive = shmi.visuals.tools.navigation.isNavigationActive(element.config.navigationTarget);
                    if (isActive) {
                        shmi.addClass(element.elements.base, "navigation-active");
                    } else {
                        shmi.removeClass(element.elements.base, "navigation-active");
                    }
                    updateSectionActiveState(self, element, isActive);
                } else {
                    shmi.removeClass(element.elements.base, "navigation-active");
                    updateSectionActiveState(self, element, false);
                }
            } else if (Array.isArray(element.children)) {
                updateNavigationState(self, disable, element.children);
            }
        });
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            listeners: [],
            activeLayer: -1,
            mobile: false,
            selected: [
                null, /* layer 0 */
                null, /* layer 1 */
                null /* layer 2 */
            ],
            crumbs: {
                layer0: null,
                layer1: null
            },
            tokens: [],
            overlay: null,
            documentListener: null
        },
        /* imports added at runtime */
        imports: {
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.crumbs.layer0 = shmi.getUiElement("selected-entry-layer-0", self.element);
                self.vars.crumbs.layer1 = shmi.getUiElement("selected-entry-layer-1", self.element);
                self.vars.menu = makeMenu(self, self.config.menu);

                shmi.removeClass(self.element, "mobile");

                self.vars.documentListener = documentListener.bind(null, self);
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this,
                    { ConditionObserver } = shmi.requires("visuals.tools.conditions"),
                    currentLayout = shmi.getCurrentLayout();

                if (self.config["mobile-layouts"].indexOf(currentLayout) !== -1) {
                    shmi.addClass(self.element, "mobile");
                    self.vars.mobile = true;
                } else {
                    shmi.removeClass(self.element, "mobile");
                    self.vars.mobile = false;
                }

                const layoutToken = shmi.listen("layout-change", function(evt) {
                    if (self.config["mobile-layouts"].indexOf(evt.detail.layout) !== -1) {
                        shmi.addClass(self.element, "mobile");
                        if (!self.vars.mobile) {
                            setLayer(self, -1);
                        }
                        self.vars.mobile = true;
                    } else {
                        shmi.removeClass(self.element, "mobile");
                        if (self.vars.mobile) {
                            setLayer(self, 0);
                        }
                        self.vars.mobile = false;
                    }
                    removeOverlay(self);
                });
                self.vars.tokens.push(layoutToken);

                if (!self.vars.mobile) {
                    setLayer(self, 0);
                }

                self.vars.listeners = collectListeners(self.vars.menu);
                self.vars.listeners.forEach((l) => l.enable());
                self.vars.tokens.push(
                    ...collectAccessData(self.vars.menu).map((reference) => new ConditionObserver(reference.condition, (() => {
                        let cb;
                        switch (reference.type) {
                        case "hide":
                            cb = makeAccessConditionCallback(reference, "hidden");
                            break;
                        case "lock":
                            cb = makeAccessConditionCallback(reference, "locked");
                            break;
                        default:
                            // Should never happen
                            throw new Error(`Invalid access type ${reference.type}`);
                        }
                        self.vars.listeners.push(cb);
                        return cb;
                    })()))
                );

                updateNavigationState(self, false, null);
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;
                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.listeners.forEach((l) => l.disable());
                self.vars.tokens = [];
                self.vars.listeners = [];
                updateNavigationState(self, true, null);
                removeOverlay(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                shmi.addClass(self.element, "locked");
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                shmi.removeClass(self.element, "locked");
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals searchbar control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-searchbar",
 *     "name": null,
 *     "template": "default/iq-searchbar.iq-variant-01",
 *     "item": null,
 *     "datagrid": null,
 *     "search-column": null,
 *     "show-results": "two-or-more",
 *     "result-columns": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * item {string}: Item alias of an item to connect. Can be set to `null` if no
 *  item is required.
 * datagrid {string}: Name of a DataGrid to search in.
 * search-column {string}: Name of the column to search in.
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 * result-columns {string[]}: Array of DataGrid column names to display in
 *  the results dropdown. `null` or an empty array will cause all columns to
 *  be included.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iqSearchbar", // control name in camel-case
        uiType = "iq-searchbar", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "iq-searchbar iq-variant-01",
        "name": null,
        "template": "default/iq-searchbar.iq-variant-01",
        "label": uiType,
        "auto-label": true,
        "item": null,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": false,
        "tooltip": null,
        "datagrid": null,
        "search-column": null,
        "show-results": "two-or-more",
        "max-results": 100,
        "max-results-hint": false,
        "result-columns": null,
        "select-contents-on-focus": true,
        "partial-word-search": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    function getMatchData(self) {
        var gData = self.vars.dataGridData || [],
            hintIdx = self.vars.currentHintIndex || 0;

        return gData[hintIdx] || null;
    }

    /**
     * Checks if all dom elements could be found.
     *
     * @param {object} dom `this.vars.dom`
     * @returns {boolean} `true` if the object did not contain any `null`
     *  values, `false` else.
     */
    function verifyDomElements(dom) {
        var iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
            ok = true;

        iterObj(dom, function(val) {
            ok = ok && val !== null;
        });

        return ok;
    }

    /**
     * Adds an event listener to the given target and stores data required to
     * remove it again in the given category.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} category Name of the category for which to register
     *  the event listener.
     * @param {EventTarget} target Target to add the event listener to.
     * @param {string} type Name of the event.
     * @param {function} listener
     */
    function addRegisteredEventListener(self, category, target, type, listener) {
        var listeners = self.vars.registeredEventListeners[category];

        // If the category doesn't exist, create it.
        if (!listeners) {
            self.vars.registeredEventListeners[category] = [];
            listeners = self.vars.registeredEventListeners[category];
        }

        target.addEventListener(type, listener);

        listeners.push({
            type: type,
            target: target,
            listener: listener
        });
    }

    /**
     * Removes all event listeners for a given category. If no category has
     * been given, event listeners for all categories are removed.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} [category] Name of the category for which to remove all
     *  event listeners.
     */
    function removeRegisteredEventListeners(self, category) {
        var iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
            listeners = [];

        if (category) {
            listeners = self.vars.registeredEventListeners[category] || [];
            delete self.vars.registeredEventListeners[category];
        } else {
            iterObj(self.vars.registeredEventListeners, function(l) {
                listeners = listeners.concat(l || []);
            });
            self.vars.registeredEventListeners = {};
        }

        listeners.forEach(function(listener) {
            listener.target.removeEventListener(listener.type, listener.listener);
        });
    }

    /**
     * Removes the displayed hint.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function clearHint(self) {
        self.vars.dom.suggestion.textContent = "";
        self.vars.currentHint = null;
    }

    /**
     * Sets a hint.
     *
     * @param {*} self Reference to a searchbar control
     * @param {?string} hint The hint
     * @param {boolean} [force] Show the hint text even if the user input field
     *  is empty.
     */
    function setHint(self, hint, force) {
        var val;

        if (!hint) {
            clearHint(self);
        } else {
            val = self.getValueDirect();

            if (val.length === 0 && !force) {
                clearHint(self);
            } else {
                if (!self.config["partial-word-search"]) {
                    self.vars.dom.suggestion.textContent = hint.substr(val.length);
                }
                self.vars.currentHint = hint;
            }
        }
    }

    /**
     * Set a hint at the given index in the current data grid data buffer.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {number} idx Index of the hint to set.
     * @param {boolean} [force] Show the hint text even if the user input field
     *  is empty.
     */
    function setHintByIdx(self, idx, force) {
        if (self.vars.dataGridData.length === 0) {
            clearHint(self);

            return;
        }

        // Make sure the index is in [0, self.vars.dataGridData)
        while (idx < 0) {
            idx += self.vars.dataGridData.length;
        }
        idx = idx % self.vars.dataGridData.length;

        if (self.vars.currentHintIndex !== null) {
            shmi.removeClass(self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex], "hint-current");
        }
        shmi.addClass(self.vars.dom.resultBox.firstChild.children[idx], "hint-current");

        setHint(self, self.vars.dataGridData[idx][self.config["search-column"]], force);
        self.vars.currentHintIndex = idx;
    }

    /**
     * Update the displayed hint. If new hints need to be fetched, this is done
     * asynchronously and the hint is cleared in the meantime.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} [inputText] String to look (and display) up hints for.
     *  If no string is given, the current string of the input field is used
     *  instead.
     */
    function updateHint(self, inputText) {
        fetchHint(self, inputText || self.getValueDirect());
    }

    /**
     * Callback called when a finished processing a new hint.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {?string} inputText String the looked up hint is meant for. If no
     *  string is given, the current string of the input field is used
     *  instead
     * @param {?string} hint The fetched hint. May be `null` if there is no
     *  hint, in which case `inputText` also isn't a valid input.
     */
    function onHint(self, inputText, hint) {
        inputText = inputText || self.getValueDirect();

        if (!hint) {
            if (inputText.length > 0) {
                shmi.addClass(self.element, "notfound");
            }
            clearHint(self);
            self.vars.lastHintSearchResult = null;
        } else {
            shmi.removeClass(self.element, "notfound");
            setHint(self, hint);
            self.vars.lastHintSearchResult = hint;
        }
    }

    /**
     * Fetches a new hint from the connected datagrid. If the given string is
     * guaranteed to yield in the same hint being found, the last search hit is
     * being used instead of actually querying the datagrid.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} inputText String to look up a hint for.
     */
    function fetchHint(self, inputText) {
        inputText = inputText.toLocaleLowerCase();
        const newValueIsMoreSpecific = self.vars.lastHintSearchToken && (inputText.indexOf(self.vars.lastHintSearchToken) === 0),
            hintMatchesNewValue = self.vars.lastHintSearchResult && (self.vars.lastHintSearchResult.toLocaleLowerCase().indexOf(inputText) === 0);

        // No need to update the hint
        if (newValueIsMoreSpecific && hintMatchesNewValue) {
            onHint(self, inputText, self.vars.lastHintSearchResult);
            self.vars.dataGridData = self.vars.dataGridData.filter((val) => val[self.config["search-column"]] && val[self.config["search-column"]].toLocaleLowerCase().indexOf(inputText) !== -1);
            updateResultBox(self, self.vars.dataGridData);

            return;
        }

        clearHint(self);
        if (self.config["partial-word-search"]) {
            self.vars.dataGrid.setFilter(self.vars.dataGridSearchColumnIdx, [ "%"+ inputText + "%" ]);
        } else {
            self.vars.dataGrid.setFilter(self.vars.dataGridSearchColumnIdx, [ inputText + "%" ]);
        }
        self.vars.lastHintSearchToken = inputText;
    }

    /**
     * Removes all nodes from the result box.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function clearResultBox(self) {
        removeRegisteredEventListeners(self, "result-box");
        while (self.vars.dom.resultBox.firstChild) {
            self.vars.dom.resultBox.removeChild(self.vars.dom.resultBox.firstChild);
        }
    }

    /**
     * Creates a new result table and attaches it to the result box.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {object[]} data DataGrid data to display.
     */
    function updateResultBox(self, data) {
        var table = document.createElement("table"),
            resultBoxVisible = false,
            columns,
            mainColumnIdx;

        clearResultBox(self);

        if (self.config["show-results"] === "never") {
            // Control is set up to never show results.
            resultBoxVisible = false;
        } else if (self.config["show-results"] === "two-or-more" && data.length <= 1) {
            // Control is set up to only show the results if there are two or
            // more. We don't have enough so stop here.
            resultBoxVisible = false;
        } else if (data.length !== 0) {
            resultBoxVisible = true;
        }

        if (resultBoxVisible) {
            shmi.addClass(self.element, "result-box-visible");
        } else {
            shmi.removeClass(self.element, "result-box-visible");
            return; //nothing left to do here
        }

        columns = Object.keys(data[0]);
        if ((self.config["result-columns"] || []).length > 0) {
            columns = columns.filter(function(columnName) {
                return self.config["result-columns"].indexOf(columnName) !== -1;
            });
        }

        // See if there are columns left we can display. If not, stop here.
        if (columns.length === 0) {
            self.vars.dom.resultBox.innerHTML = "No columns to display. Please check <span style=\"font-weight: bold\">result-columns</span> in your control configuration.";
            fLog("No columns to display.", "dataGrid columns =", Object.keys(data[0]), "columns to display =", self.config["result-columns"]);
            return;
        }

        // Make sure to `search-column` is at position 0 in the columns array
        // (= it is displayed first).
        mainColumnIdx = columns.indexOf(self.config["search-column"]);
        if (mainColumnIdx > 0) {
            columns.unshift(columns[mainColumnIdx]);
            columns.splice(mainColumnIdx + 1, 1);
        }

        // Fill the table.
        data.forEach(function(row, idx) {
            var tr = document.createElement("tr");
            columns.forEach(function(columnName) {
                var cell = document.createElement("td");
                cell.textContent = row[columnName] || "";

                tr.appendChild(cell);
            });

            addRegisteredEventListener(self, "result-box", tr, "touchstart", (evt) => {
                evt.stopPropagation();
            });
            addRegisteredEventListener(self, "result-box", tr, "mousedown", function(ev) {
                self.setValue(row[self.config["search-column"]]);
                shmi.removeClass(self.element, "focused");
            });

            addRegisteredEventListener(self, "result-box", tr, "mouseover", setHintByIdx.bind(null, self, idx, true));
            addRegisteredEventListener(self, "result-box", tr, "mouseleave", shmi.removeClass.bind(shmi, tr, "hint-current"));

            table.appendChild(tr);
        });

        if (self.config["max-results-hint"] && (data.length === self.config["max-results"]) && (self.vars.dataGridResults > data.length)) {
            const row = document.createElement("tr");
            row.classList.add("max-results-notice");
            columns.forEach((colName, idx) => {
                const cell = document.createElement("td");
                if (idx === 0) {
                    const noticeText = shmi.localize("${iqsearchbar.results.moreAvailable}");
                    cell.textContent = noticeText;
                    cell.setAttribute("title", noticeText);
                }

                row.appendChild(cell);
            });
            table.appendChild(row);
        }

        self.vars.dom.resultBox.appendChild(table);
    }

    /**
     * Scrolls the result box to the given element. Will not scroll, if the
     * element is already visible. After a scroll, the element will be top
     * aligned if the result box was scrolled up and down aligned if the result
     * box was scrolled down.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {DOMElement} element Element to scroll the result box to.
     */
    function doResultBoxScroll(self, element) {
        var scrollTop = self.vars.dom.resultBox.scrollTop,
            scrollBottom = scrollTop + self.vars.dom.resultBox.offsetHeight;

        if (element.offsetTop < scrollTop) {
            element.scrollIntoView(true);
        } else if (element.offsetTop + element.offsetHeight > scrollBottom) {
            element.scrollIntoView(false);
        }
    }

    /**
     * Returns a datagrids data in a more convenient format.
     *
     * @param {DataGrid} dg Data grid to get data from.
     * @param {number} subID Subscription id.
     * @returns {object[]} Array of rows where each row is key-, value store.
     */
    function getDataGridDataHelper(dg, subID) {
        var fieldNames = dg.getFields();

        return dg.getCurrentIDs(subID).map(function(id) {
            var row = dg.getRowData(id),
                rowdata = {};

            if (!row) {
                return null;
            }

            row.forEach(function(cell, cellIdx) {
                rowdata[fieldNames[cellIdx]] = String(cell.value);
            });

            return rowdata;
        });
    }

    /**
     * Subscriber function attached to the searchbars datagrid.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {object} dgData DataGrid update notification data.
     */
    function hintFetchCompleteCallback(self, dgData) {
        const inputText = self.getValueDirect(),
            rowData = getDataGridDataHelper(self.vars.dataGrid, self.vars.dataGridSubscriptionId.id);

        let hint;

        // No hint found?
        if (rowData.length > 0) {
            hint = (rowData[0] || {})[self.config["search-column"]];
        }

        self.vars.currentHintIndex = null;
        self.vars.dataGridData = rowData;
        self.vars.dataGridResults = dgData.totalRows;
        onHint(self, inputText, hint || null);
        updateResultBox(self, rowData);
    }

    /**
     * Set the current selection to the end of the given elements content.
     *
     * @param {DOMElement} element
     * @param {boolean} selectContent Selects the entire content instead of
     *  just the end.
     */
    function setSelected(element, selectContent) {
        // Setting textContent reset our cursor position. Set the
        // cursor to the end of the content.
        var range = document.createRange();
        range.selectNodeContents(element);
        if (!selectContent) {
            range.setStart(element, range.endOffset);
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * Event listener callback attached to the user input field.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {KeyboardEvent} ev
     */
    function userInputKeyDownHandler(self, ev) {
        if (ev.key === "Tab") {
            // On "Tab" use autocompletion if appropriate. Prevent the event
            // from propagating if autocompletion was used but don't otherwise.
            if (self.vars.currentHint && self.getValueDirect() !== self.vars.currentHint) {
                ev.preventDefault();

                self.setValue(self.vars.currentHint);
                clearHint(self);

                // Setting textContent reset our cursor position. Set the
                // cursor to the end of the content.
                setSelected(self.vars.dom.input);
            }
        } else if (ev.key === "Escape") {
            // On "Escape" toggle the result box.
            if (shmi.hasClass(self.element, "show-results")) {
                shmi.removeClass(self.element, "show-results");
            } else {
                shmi.addClass(self.element, "show-results");
            }
        } else if (ev.key === "ArrowUp") {
            // On "ArrowUp" scroll up by 1.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, (self.vars.currentHintIndex || 0) - 1, true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "PageUp") {
            // On "PageUp" scroll up by 10.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, Math.max((self.vars.currentHintIndex || 0) - 10, 0), true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "ArrowDown") {
            // On "ArrowDown" scroll down by 1.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, (self.vars.currentHintIndex || 0) + 1, true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "PageDown") {
            // On "PageDown" scroll down by 10.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, Math.min((self.vars.currentHintIndex || 0) + 10, self.vars.dataGridData.length - 1), true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "Enter") {
            // On "Return" or "Enter" remove focus and autocomplete.
            if (self.vars.currentHint) {
                self.setValue(self.vars.currentHint);
            }

            ev.target.blur();
        }
    }

    /**
     * Attaches event listeners to the user input field.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function setupDomUserInput(self) {
        // Update the hint whenever the content of the user input field
        // changes.
        addRegisteredEventListener(self, "input", self.vars.dom.input, "input", function onInput() {
            var inputText = self.getValueDirect();

            self.vars.dom.mirror.textContent = inputText;

            updateHint(self, inputText);
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "keydown", userInputKeyDownHandler.bind(null, self));

        addRegisteredEventListener(self, "input", self.vars.dom.input, "focus", function onFocus() {
            self.fire("enter", { value: self.getValue() });
            self.vars.userInputActive = true;
            shmi.addClass(self.element, "focused");
            shmi.addClass(self.element, "show-results");
            updateHint(self);

            if (self.config["select-contents-on-focus"]) {
                setSelected(self.vars.dom.input, true);
            }
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "blur", function onBlur() {
            const value = self.getValueDirect();

            self.vars.userInputActive = false;
            self.vars.lastValue = value;
            shmi.removeClass(self.element, "focused");
            window.getSelection().removeAllRanges();
            clearHint(self);

            if (self.vars.itemSubscription && self.config.item) {
                self.imports.im.writeValue(self.config.item, value);
            }

            self.fire("change", { value });
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "touchstart", function onDown(evt) {
            showKeyboard(self);
            evt.stopPropagation();
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "click", function onClick() {
            showKeyboard(self);
            shmi.addClass(self.element, "show-results");
        });
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.dom.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.dom.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.dom.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Show the alphanumeric keyboard if it is enabled
     *
     * @param {*} self Reference to the widget
     */
    function showKeyboard(self) {
        const appConfig = shmi.requires("visuals.session.config"),
            keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

        if (!keyboardEnabled) {
            return;
        }

        shmi.keyboard(
            {
                "value": self.getValueDirect(),
                "select-box-enabled": appConfig.keyboard["language-selection"],
                "show-enter": false,
                "callback": function(success, input) {
                    if (success) {
                        self.setValue(input);
                        shmi.addClass(self.element, "focused");
                    }
                }
            });
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dom: {
                icon: null,
                label: null,
                input: null,
                mirror: null,
                suggestion: null,
                resultBox: null,
                clear: null
            },
            label: null,
            initDone: false,
            registeredEventListeners: {},
            dataGrid: null,
            dataGridSubscriptionId: null,
            dataGridSearchColumnIdx: -1,
            dataGridData: [],
            dataGridResults: 0,
            currentHint: null,
            currentValue: null,
            lastHintSearchToken: null,
            lastHintSearchResult: null,
            lastValue: null,
            userInputActive: false,
            currentHintIndex: null,
            itemSubscription: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change", "enter"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    io = shmi.requires("visuals.io"),
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                this.vars.dom.icon = shmi.getUiElement("icon", this.element);
                this.vars.dom.label = shmi.getUiElement("label", this.element);
                this.vars.dom.input = shmi.getUiElement("searchbar-user-input", this.element);
                this.vars.dom.mirror = shmi.getUiElement("searchbar-mirror", this.element);
                this.vars.dom.suggestion = shmi.getUiElement("searchbar-suggestion", this.element);
                this.vars.dom.resultBox = shmi.getUiElement("searchbar-result-box", this.element);
                this.vars.dom.clear = shmi.getUiElement("clear-input", this.element);
                this.vars.dataGrid = this.imports.dgm.getGrid(this.config.datagrid);

                if (!verifyDomElements(this.vars.dom)) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                // Icon
                if (!this.vars.dom.icon) {
                    shmi.log('[IQ:iq-input-field] no button-icon element provided', 1);
                } else if (this.config['icon-src'] && this.config['show-icon']) {
                    applyIcon(self.vars.dom.icon, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                    shmi.addClass(self.vars.dom.icon, "inlined");
                } else if (this.config['icon-class'] && this.config['show-icon']) {
                    applyIcon(self.vars.dom.icon, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(this.element, "no-icon");
                }

                // Label
                setLabelImpl(this, this.config.label);

                const domLabel = shmi.getUiElement("searchbar-label", this.element);
                if (domLabel) {
                    if (this.config.label && String(this.config.label).length > 0) {
                        domLabel.textContent = shmi.localize(this.config.label);
                    } else {
                        domLabel.style.display = "none";
                    }
                }

                const clearHandler = {
                    onClick: function() {
                        if (self.isActive() && !self.locked) {
                            self.setValue("");
                        }
                    }
                };
                self.vars.listeners.push(new io.MouseListener(self.vars.dom.clear, clearHandler));
                self.vars.listeners.push(new io.TouchListener(self.vars.dom.clear, clearHandler));

                if (this.vars.dataGrid === null) {
                    this.element.textContent = "Unable to find datagrid or no datagrid configured. This control will not work. Please select a valid datagrid.";
                    if (this.config.datagrid) {
                        fLog("Datagrid not found", this.config.datagrid);
                    } else {
                        fLog("No datagrid configured");
                    }

                    return;
                }

                this.vars.initDone = true;

                this.vars.dataGridSearchColumnIdx = this.vars.dataGrid.getFields().indexOf(this.config["search-column"]);
                if (this.vars.dataGridSearchColumnIdx === -1) {
                    this.element.textContent = "Datagrid does not have the configured field to search in. This control will not work. Please select a valid search field.";
                    fLog("Datagrid does not have the configured search field.");
                }

                this.vars.dataGrid.sort(this.vars.dataGridSearchColumnIdx, "ASC");
            },
            /**
             * Returns the value of the user input field.
             *
             * @returns {string}
             */
            getValue: function() {
                return this.vars.lastValue;
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {string} Value of the input field.
             */
            getValueDirect: function() {
                if (!this.vars.initDone) {
                    return null;
                }

                const value = this.vars.dom.input.value;

                // Replace non breaking spaces with normal ones
                return value.replace(String.fromCharCode(0xA0), " ");
            },
            getMatchData: function() {
                return getMatchData(this);
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                if (!this.vars.initDone) {
                    return;
                }

                this.vars.dom.input.value = val;
                this.vars.dom.mirror.textContent = val;
                this.vars.lastValue = val;
                this.fire("change", { value: this.getValue() });
                updateHint(this, val);
            },
            /* called when control is enabled */
            onEnable: function() {
                if (!this.vars.initDone) {
                    return;
                }
                const maxResults = typeof this.config["max-results"] === "number" && this.config["max-results"] > 0 ? this.config["max-results"] : 100;

                setupDomUserInput(this);

                this.vars.dataGridSubscriptionId = this.vars.dataGrid.subscribePage(0, maxResults, hintFetchCompleteCallback.bind(null, this));
                if (this.config.item) {
                    this.vars.itemSubscription = this.vars.im.subscribeItem(this.config.item, this);
                }
                this.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                if (!this.vars.initDone) {
                    return;
                }

                this.vars.dataGrid.unsubscribe(this.vars.dataGridSubscriptionId.id);
                this.vars.dataGridSubscriptionId = null;

                if (this.vars.itemSubscription) {
                    this.vars.itemSubscription.unlisten();
                    this.vars.itemSubscription = null;
                }

                clearResultBox(this);
                removeRegisteredEventListeners(this);
                this.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                this.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                this.vars.dom.input.disabled = true;
                shmi.addClass(this.element, "locked");
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                this.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                this.vars.dom.input.disabled = false;
                shmi.removeClass(this.element, "locked");
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                if (this.vars.initDone && !this.vars.userInputActive) {
                    this.setValue(value);
                }
            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            },
            setLabel: function(labelText) {
                if (this.vars.dom.label && this.config['auto-label']) {
                    setLabelImpl(this, labelText, false);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-box
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-box",
 *     "name": null,
 *     "template": "default/iq-select-box.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 * "no-selection-label": The default "no selection" label
 * "selected": Which option is selected
 * "options": The options
 * "item": The item
 * "show-icon": Whether to show the icon
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-select-box", // control name in camel-case
        uiType = "iq-select-box", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-select-box iq-variant-01",
        "name": null,
        "template": "default/iq-select-box.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "no-selection-label": "---",
        "selected": -1,
        "popup-location": "bottom",
        "options": [],
        "show-text": true,
        "show-icon": true,
        "inline-svg": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            optionListeners: [],
            domListeners: {},
            value: 0,
            active: false,

            // DOM Elements
            templateEl: null,
            labelEl: null,
            iconEl: null,
            containerEl: null,
            anchorEl: null,
            optionEl: null,
            selectedEl: null,
            selectedValEl: null,
            optionEls: [],

            selected: -1,
            clickedInside: false,
            isOpen: false,
            isLocked: false,
            highlightIndex: null,
            subscriptionTargetId: null,

            // Dynamic CSS classes
            selectBoxSelectedClass: 'option-selected'
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            iter: "visuals.tools.iterate.iterateObject"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.templateEl = self.element.cloneNode(true);

                self.vars.iqWidgetContent = shmi.getUiElement('iq-widget-content', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                self.vars.containerEl = shmi.getUiElement('options', self.element);
                self.vars.selectedEl = shmi.getUiElement('selected', self.element);
                self.vars.selectedValEl = shmi.getUiElement('value', self.vars.selectedEl);
                self.vars.optionEl = shmi.getUiElement('option', self.element);
                self.vars.anchorEl = shmi.getUiElement('anchor', self.vars.containerEl);

                if (!self.vars.containerEl || !self.vars.anchorEl || !self.vars.optionEl || !self.vars.selectedEl || !self.vars.selectedValEl) {
                    shmi.log('[IQ:iq-select-box] At least one element is missing: containerEl, anchorEl, optionEl, selectedEl, selectedValEl', 3);
                    return;
                }

                // remove template DOM nodes and remap option elements to template
                self.vars.iqWidgetContent.removeChild(self.vars.selectedEl);
                self.vars.iqWidgetContent.removeChild(self.vars.containerEl);
                self.vars.iqWidgetContent.removeChild(self.vars.optionEl);

                self.vars.containerEl = shmi.getUiElement('options', self.vars.templateEl);
                self.vars.anchorEl = shmi.getUiElement('anchor', self.vars.containerEl);
                self.vars.optionEl = shmi.getUiElement('option', self.vars.templateEl);
                self.vars.selectedEl = shmi.getUiElement('selected', self.vars.templateEl);
                self.vars.selectedValEl = shmi.getUiElement('value', self.vars.selectedEl);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-select-box] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Label
                if (self.config['show-text']) {
                    setLabelImpl(self, self.config.label);
                } else {
                    setLabelImpl(self, null);
                }

                const popupContainerEl = document.createElement("DIV");
                shmi.addClass(popupContainerEl, 'iqel-popup-container');
                self.vars.iqWidgetContent.appendChild(popupContainerEl);
                popupContainerEl.appendChild(self.vars.containerEl);
                popupContainerEl.appendChild(self.vars.selectedEl);

                self.rebuildOptions(false);

                self.vars.selected = self.config.selected;
                self.vars.containerEl.setAttribute('tabindex', '-1');

                if (self.config["popup-location"] === 'top') {
                    shmi.addClass(self.element, 'list-on-top');
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                self.vars.domListeners = self.createDomListenerObject();
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.element, self.createElementListenerObject()));
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.element, self.createElementListenerObject()));
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.containerEl, self.createContainerListenerObject()));

                // Defaults
                shmi.addClass(self.vars.containerEl, 'hidden');
                if (self.vars.selected === -1) {
                    self.setSelected(null);
                } else {
                    self.setSelected(self.vars.optionEls[self.config.selected]);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.element.setAttribute('tabindex', 0);

                self.lockUnlockListeners(false);

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-select-box] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.lockUnlockListeners(true);

                if (self.vars.subscriptionTargetId) {
                    self.vars.subscriptionTargetId.unlisten();
                    self.vars.subscriptionTargetId = null;
                }

                shmi.log("[IQ:iq-select-box] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.isLocked = true;
                shmi.addClass(self.vars.containerEl, 'hidden');
                self.vars.highlightIndex = -1;

                self.lockUnlockListeners(true);

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-box] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.lockUnlockListeners(false);

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-box] unlocked", 1);
            },

            setOptions: function(options) {
                const self = this;

                self.config.options = options;
                if (self.initialized) {
                    self.rebuildOptions(self.isActive());
                    self.setValue(self.getValue());
                }
            },

            getOptions: function() {
                const self = this;
                return shmi.cloneObject(self.config.options);
            },

            /**
             * Sets the selected element
             *
             * @param element - element to select
             */
            setSelected: function(element) {
                const self = this,
                    selectedIdx = self.vars.optionEls.findIndex((el) => el === element);

                // Unselect all
                self.vars.optionEls.forEach(function(val, idx) {
                    shmi.removeClass(val, self.vars.selectBoxSelectedClass);
                });

                if (selectedIdx === -1) {
                    self.vars.selectedValEl.textContent = shmi.localize(self.config['no-selection-label']);
                    self.vars.value = null;
                    const iconEl = shmi.getUiElement("option-icon", self.vars.selectedEl);
                    if (iconEl) {
                        self.setIcon(iconEl, {}, true);
                    }
                } else {
                    // Get selected label
                    const selectedLabel = self.config.options[selectedIdx].label;

                    shmi.addClass(element, self.vars.selectBoxSelectedClass);
                    self.vars.selectedValEl.textContent = shmi.localize(selectedLabel);
                    self.vars.value = self.config.options[selectedIdx].value;

                    let selectedOption = self.config.options[selectedIdx],
                        iconEl = shmi.getUiElement("option-icon", self.vars.selectedEl);

                    if (selectedOption && iconEl) {
                        self.setIcon(iconEl, selectedOption, true);
                    }
                }
                self.vars.selected = selectedIdx;
            },

            getCurrentIndex: function() {
                const self = this;

                return self.vars.selected;
            },

            /**
             * Writes the current value to a connected data-source item
             *
             */
            updateValue: function() {
                const self = this;
                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, self.getValue());
                }
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    oldValue = self.getValue();

                shmi.log("[IQ:iq-select-box] Setting value...: " + value, 0);

                const selectedIndex = self.config.options.findIndex((option) => option.value === value);
                self.setSelected(selectedIndex !== -1 ? self.vars.optionEls[selectedIndex] : null);

                if (self.getValue() !== oldValue) {
                    self.fire("change", {
                        value: self.getValue()
                    });
                }
                shmi.log("[IQ:iq-select-box] Value set: " + value, 1);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /**
             * Called when the label is received
             *
             * @param labelText
             */
            setLabel: function(labelText) {
                const self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Helper function for locking/unlocking
             *
             * @param {boolean} lock
             */
            lockUnlockListeners: function(lock) {
                const self = this;

                // SHMI listeners
                self.vars.listeners.forEach(function(l) {
                    if (lock) {
                        l.disable();
                    } else {
                        l.enable();
                    }
                });

                self.vars.optionListeners.forEach(function(l) {
                    if (lock) {
                        l.disable();
                    } else {
                        l.enable();
                    }
                });

                // DOM Listeners
                self.imports.iter(self.vars.domListeners, function(val, prop) {
                    if (val.element) {
                        if (lock) {
                            val.element.removeEventListener(val.event, val.callback);
                        } else {
                            val.element.addEventListener(val.event, val.callback);
                        }
                    }
                });
            },

            /**
             * Rebuilds options
             *
             * @param enableListeners
             */
            rebuildOptions: function(enableListeners) {
                const self = this;

                // Remove listeners
                self.vars.optionListeners.forEach(function(listener) {
                    listener.disable();
                });
                self.vars.optionListeners = [];

                // Reset option-elements in case of call to 'setOptions(..)'
                self.vars.optionEls = [];

                // Clear old option elements
                while (self.vars.anchorEl.firstChild) {
                    self.vars.anchorEl.removeChild(self.vars.anchorEl.firstChild);
                }

                self.config.options.forEach(function(option, index) {
                    let label = shmi.localize(option.label),
                        optionEl = self.vars.optionEl.cloneNode(true),
                        valueEl = shmi.getUiElement('value', optionEl),
                        iconEl = shmi.getUiElement('icon', optionEl);

                    if (valueEl) {
                        valueEl.textContent = label;
                    } else {
                        optionEl.textContent = label;
                    }

                    if (option["tooltip"] || option["icon-title"]) {
                        optionEl.setAttribute("title", shmi.localize(option.tooltip || option["icon-title"]));
                    }

                    if (iconEl) {
                        self.setIcon(iconEl, option, false);
                    }

                    self.vars.anchorEl.appendChild(optionEl);
                    self.vars.optionEls.push(optionEl);
                });

                const mouseHandler = self.makeOptionMouseHandler(),
                    touchHandler = self.makeOptionTouchHandler();

                self.vars.optionEls.forEach(function(el) {
                    self.vars.optionListeners.push(new shmi.visuals.io.TouchListener(el, touchHandler));
                    self.vars.optionListeners.push(new shmi.visuals.io.MouseListener(el, mouseHandler));
                });

                if (enableListeners) {
                    self.vars.optionListeners.forEach(function(listener) {
                        listener.enable();
                    });
                }
            },

            /**
             * Sets the icon
             *
             * @param {object} element
             * @param {object} option
             * @param {boolean} isSelected
             *
             * @returns {boolean}
             */
            setIcon: function(element, option, isSelected) {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");
                let hasIcon = false;

                if (option["icon-src"]) {
                    applyIcon(element, option["icon-src"], null, self.config["inline-svg"]);
                    hasIcon = true;
                } else if (option["icon-class"]) {
                    applyIcon(element, null, option["icon-class"]);
                    hasIcon = true;
                }

                element.dataset.ui = "option-icon";

                if (!hasIcon) {
                    shmi.addClass(element, "hidden");
                } else {
                    shmi.removeClass(element, "hidden");
                }

                return hasIcon;
            },

            /**
             * Creates mouse listener functions to be attached to each option element.
             */
            makeOptionMouseHandler: function() {
                const self = this;
                return {
                    onClick: function onClick(x, y, e) {
                        shmi.log("[IQ:iq-select-box] Click on element", 0);
                        const oldValue = self.getValue();

                        self.setSelected(e.currentTarget);
                        self.updateValue();

                        shmi.addClass(self.vars.containerEl, 'hidden');

                        self.vars.isOpen = false;
                        self.vars.highlightIndex = -1;
                        if (self.getValue() !== oldValue) {
                            self.fire("change", {
                                value: self.getValue()
                            });
                        }
                    },

                    onPress: function onPress(x, y, e) {
                        shmi.log("[IQ:iq-select-box] Press", 0);
                        e.preventDefault();
                    },

                    onRelease: function onRelease() {
                        shmi.log("[IQ:iq-select-box] Release", 0);
                    }
                };
            },

            /**
             * Creates touch listener functions to be attached to each option element.
             */
            makeOptionTouchHandler: function() {
                const self = this;
                return {
                    onPress: function onPress() {
                    },
                    onClick: function onClick(x, y, e) {
                        const oldValue = self.getValue();

                        shmi.log("[IQ:iq-select-box] Click on element", 0);

                        self.setSelected(e.currentTarget);
                        self.updateValue();

                        shmi.addClass(self.vars.containerEl, 'hidden');
                        self.vars.isOpen = false;
                        self.vars.highlightIndex = -1;

                        if (self.getValue() !== oldValue) {
                            self.fire("change", {
                                value: self.getValue()
                            });
                        }
                    }
                };
            },

            /**
             * Generates the listener object
             *
             * @returns {object}
             */
            createDomListenerObject: function() {
                const self = this;
                return {
                    containerBlur: {
                        "event": 'blur',
                        "element": self.vars.containerEl,
                        "callback": function(e) {
                            if (self.vars.clickedInside) {
                                setTimeout(function() {
                                    self.vars.containerEl.focus();
                                }, shmi.c("DECOUPLE_TIMEOUT"));
                            } else {
                                shmi.addClass(self.vars.containerEl, 'hidden');
                                setTimeout(function() {
                                    self.vars.isOpen = false;
                                }, shmi.c("DECOUPLE_TIMEOUT"));
                                self.vars.highlightIndex = -1;
                            }
                        }
                    },
                    elementKeydown: {
                        "event": 'keydown',
                        "element": self.element,
                        "callback": function(e) { // Add keyboard listener to select box to be opened on ENTER
                            const key = e.key ? e.key : e.code,
                                focusedNotLocked = (this.element === document.activeElement) && !this.locked;
                            if (focusedNotLocked && (key === 'Enter' || key === 'NumpadEnter')) {
                                shmi.log("[IQ:iq-select-box] click on container", 1);
                                if (self.vars.isOpen) {
                                    self.vars.containerEl.blur();
                                } else {
                                    shmi.removeClass(self.vars.containerEl, 'hidden');
                                    self.vars.isOpen = true;
                                    setTimeout(function() {
                                        self.vars.containerEl.focus();
                                        shmi.visuals.session.FocusElement = self.vars.containerEl;
                                    }, shmi.c("DECOUPLE_TIMEOUT"));
                                }
                            }
                        }
                    },
                    containerKeydown: {
                        "event": "keydown",
                        "element": self.vars.containerEl,
                        "callback": function(e) {
                            const key = e.key ? e.key : e.code;
                            shmi.log("[IQ:iq-select-box] Keydown event: " + key, 1);
                            if (self.vars.highlightIndex < 0) {
                                self.vars.highlightIndex = self.getCurrentIndex();
                            }
                            shmi.log("[IQ:iq-select-box] Highlight index " + self.vars.highlightIndex, 1);
                            if (key === "Escape") {
                                self.vars.optionEls.forEach(function(optionEl) {
                                    shmi.removeClass(optionEl, 'highlighted');
                                });
                                shmi.addClass(self.vars.containerEl, 'hidden');
                                self.vars.isOpen = false;
                                self.vars.highlightIndex = -1;
                            } else if (key === "Enter" || key === "NumpadEnter") {
                                if (self.vars.highlightIndex !== -1) {
                                    shmi.log("[IQ:iq-select-box] Click on element", 0);
                                    const element = self.vars.optionEls[self.vars.highlightIndex],
                                        oldValue = self.getValue();

                                    self.setSelected(element);
                                    self.updateValue();
                                    shmi.addClass(self.vars.containerEl, 'hidden');
                                    self.vars.isOpen = false;
                                    self.vars.highlightIndex = -1;
                                    if (self.getValue() !== oldValue) {
                                        self.fire("change", { value: self.getValue() });
                                    }
                                    self.vars.optionEls.forEach(function(optionEl) {
                                        shmi.removeClass(optionEl, 'highlighted');
                                    });
                                    self.vars.highlightIndex = -1;
                                }
                            } else if (key === "ArrowUp") {
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.removeClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                                if (self.vars.highlightIndex > 0) {
                                    self.vars.highlightIndex--;
                                } else {
                                    self.vars.highlightIndex = self.vars.optionEls.length - 1;
                                }
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.addClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                            } else if (key === "ArrowDown") {
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.removeClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                                if (self.vars.highlightIndex < (self.vars.optionEls.length - 1)) {
                                    self.vars.highlightIndex++;
                                } else {
                                    self.vars.highlightIndex = 0;
                                }
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.addClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                            }
                            return false;
                        }
                    }
                };
            },

            /**
             * Generates SHMI listener object
             *
             * @returns {{onPress: onPress, onClick: onClick}}
             */
            createElementListenerObject: function() {
                const self = this;
                return {
                    onPress: function() {
                        // Left empty
                    },
                    onClick: function(x, y, e) {
                        if (e.target !== self.vars.anchorEl) {
                            shmi.log("[IQ:is-select-box] Click on container", 1);
                            if (self.vars.isOpen) {
                                self.vars.containerEl.blur();
                            } else {
                                shmi.removeClass(self.vars.containerEl, 'hidden');
                                self.vars.isOpen = true;
                                setTimeout(function() {
                                    self.vars.containerEl.focus();
                                    shmi.visuals.session.FocusElement = self.vars.containerEl;
                                }, shmi.c("DECOUPLE_TIMEOUT"));
                            }
                        }
                    }
                };
            },

            /**
             * Creates SHMI listener object
             *
             * @returns {{onPress: onPress, onRelease: onRelease, onLeave: onLeave}}
             */
            createContainerListenerObject: function() {
                const self = this;
                return {
                    onPress: function(x, y, e) {
                        if (e.currentTarget === self.vars.containerEl) {
                            self.vars.clickedInside = true;
                        }
                    },
                    onRelease: function(x, y, e) {
                        if (e.currentTarget === self.vars.containerEl) {
                            self.vars.clickedInside = false;
                        }
                    },
                    onLeave: function() {
                        shmi.log("[IQ:iq-select-box] onLeave container element", 0);
                        self.vars.containerEl.blur();
                        self.vars.highlightIndex = -1;
                    }
                };
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-select-date
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-date",
 *     "name": null,
 *     "template": "default/iq-select-date"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "tooltip": Tooltip
 * "dateformat": The date format
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-select-date", // control name in camel-case
        uiType = "iq-select-date", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-select-date iq-variant-01",
        "name": null,
        "template": "default/iq-select-date.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "tooltip": null,
        "dateformat": "${V_DATEFORMAT}",
        "daynames": [
            '${V_SUN_SHORT}',
            '${V_MON_SHORT}',
            '${V_TUE_SHORT}',
            '${V_WED_SHORT}',
            '${V_THU_SHORT}',
            '${V_FRI_SHORT}',
            '${V_SAT_SHORT}'
        ],
        "daynames-long": [
            '${V_SUN}',
            '${V_MON}',
            '${V_TUE}',
            '${V_WED}',
            '${V_THU}',
            '${V_FRI}',
            '${V_SAT}'
        ],
        "monthnames": [
            '${V_JAN}',
            '${V_FEB}',
            '${V_MAR}',
            '${V_APR}',
            '${V_MAY}',
            '${V_JUN}',
            '${V_JUL}',
            '${V_AUG}',
            '${V_SEP}',
            '${V_OCT}',
            '${V_NOV}',
            '${V_DEC}'
        ],
        "show-icon": true,
        "show-text": true,
        "icon-src": "pics/system/controls/iq-select-date/calendar-icon.svg",
        "icon-class": null,
        "icon-title": null,
        "inline-svg": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * check if current layout is in modal display mode
     *
     * @param {object} self instance reference
     * @return {boolean} `true` if modal layout is active, `false` else
     */
    function isModal(self) {
        if (self.vars.calendarEl) {
            const style = getComputedStyle(self.vars.calendarEl);
            if (["fixed", "absolute"].includes(style.position)) {
                return true;
            }
        }

        return false;
    }

    /**
     * initialize listeners for ok-/cancel-buttons
     *
     * @param {object} self instance reference
     */
    function initButtonListeners(self) {
        if (self.vars.okButton) {
            self.vars.tokens.push(
                self.vars.okButton.listen("click", () => {
                    applyDate(self);
                    shmi.removeClass(self.element, "open");
                    self.vars.isOpen = false;
                })
            );
        }

        if (self.vars.cancelButton) {
            self.vars.tokens.push(
                self.vars.cancelButton.listen("click", () => {
                    const date = new Date(self.vars.value * 1000);
                    self.vars.date.setFullYear(date.getFullYear(), date.getMonth());
                    self.vars.selectedDate.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
                    shmi.removeClass(self.element, "open");
                    self.vars.isOpen = false;
                })
            );
        }
    }

    /**
     * apply selected date as current value
     *
     * @param {object} self instance reference
     */
    function applyDate(self) {
        const oldValue = self.vars.value;
        self.vars.value = self.vars.tempValue;
        updateDisplayDate(self);

        if (self.config.item) {
            self.imports.im.writeValue(self.config.item, self.vars.value);
        }

        if (oldValue !== self.vars.value) {
            self.fire("change", {
                value: self.vars.value
            });
        }
    }

    /**
     * update displayed current date text
     *
     * @param {object} self instance reference
     */
    function updateDisplayDate(self) {
        const currentDate = shmi.localize(self.config.dateformat);
        if (typeof (currentDate) === 'string') {
            self.vars.selectedEl.textContent = self.imports.dt.formatDateTime(self.vars.selectedDate, { datestring: currentDate });
        } else {
            self.vars.selectedEl.innerHTML = '&nbsp;';
        }
    }

    /**
     * get 'class-name' option value for created buttons
     *
     * @param {object} self instance reference
     * @param {string[]} [additionalSelectors=[]] additional css-selectors to include
     * @return {string} 'class-name' option value
     */
    function getButtonClassName(self, additionalSelectors = []) {
        if (self.element.classList.contains("secondary")) {
            additionalSelectors.push("secondary");
        } else if (self.element.classList.contains("tertiary")) {
            additionalSelectors.push("tertiary");
        }

        return `iq-button iq-variant-01${additionalSelectors?.length ? " " + additionalSelectors.join(" ") : ""}`;
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            tokens: [],
            value: 0,
            tempValue: 0,
            active: false,
            isOpen: true,

            // DOM Elements
            labelEl: null,
            calendarEl: null,
            daySelectEl: null,
            headerEl: null,
            dayNameEls: null,
            selectedEl: null,
            dayEls: [],

            monthSelectEl: null,
            previousMonthEl: null,
            currentMonthEl: null,
            nextMonthEl: null,

            yearSelectEl: null,
            previousYearEl: null,
            currentYearEl: null,
            nextYearEl: null,

            pressedEl: null,

            // Event Handler
            subscriptionTargetId: null,

            // Vars
            date: null,
            selectedDate: null,
            isModal: false,
            buttonAnchor: null,
            okButton: null,
            cancelButton: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues",
            dt: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                self.initDate();

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.calendarEl = shmi.getUiElement('calendar', self.element);
                self.vars.daySelectEl = shmi.getUiElement('day-select', self.element);
                self.vars.headerEl = shmi.getUiElement('day-header', self.element);
                self.vars.yearSelectEl = shmi.getUiElement('year-select', self.element);

                self.vars.dayNameEls = shmi.getUiElements('day-name', self.vars.headerEl);
                self.vars.dayEls = shmi.getUiElements('day', self.element);
                self.vars.valueEl = shmi.getUiElement('value', self.element);

                self.vars.selectedEl = shmi.getUiElement('selected-date', self.element);
                if (self.vars.valueEl) {
                    self.vars.selectedEl = self.vars.valueEl;
                }

                self.vars.monthSelectEl = shmi.getUiElement('month-select', self.element);
                if (self.vars.monthSelectEl) {
                    self.vars.previousMonthEl = shmi.getUiElement('previous', self.vars.monthSelectEl);
                    self.vars.currentMonthEl = shmi.getUiElement('current', self.vars.monthSelectEl);
                    self.vars.nextMonthEl = shmi.getUiElement('next', self.vars.monthSelectEl);
                }

                if (self.vars.yearSelectEl) {
                    self.vars.previousYearEl = shmi.getUiElement('previous', self.vars.yearSelectEl);
                    self.vars.currentYearEl = shmi.getUiElement('current', self.vars.yearSelectEl);
                    self.vars.nextYearEl = shmi.getUiElement('next', self.vars.yearSelectEl);
                }

                if (!self.vars.calendarEl || !self.vars.daySelectEl || !self.vars.selectedEl || !self.vars.headerEl || !self.vars.monthSelectEl || !self.vars.yearSelectEl) {
                    shmi.log('[IQ:iq-select-date] No calendar/day-select/selected-date/value/day-header/month-select/year-select element provided', 3);
                    return;
                }

                if (!self.vars.dayNameEls || (self.vars.dayNameEls && self.vars.dayNameEls.length !== 7)) {
                    shmi.log('[IQ:iq-select-date] No day-name elements, or not the right count (7) provided', 3);
                    return;
                }

                if (!(self.vars.previousMonthEl && self.vars.currentMonthEl && self.vars.nextMonthEl && self.vars.previousYearEl && self.vars.currentYearEl && self.vars.nextYearEl)) {
                    shmi.log('[IQ:iq-select-date] Not all month & year select elements provided', 3);
                    return;
                }

                self.vars.dayNameEls.forEach(function(el, idx) {
                    el.textContent = shmi.localize(self.config.daynames[(idx === 6) ? 0 : idx + 1]);
                });

                self.vars.calendarEl.setAttribute('tabindex', "0");

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-select-date] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg"]);
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, 'no-icon');
                }

                self.vars.isModal = isModal(self);

                if (self.vars.isModal) {
                    self.vars.buttonAnchor = shmi.getUiElement("button-anchor", self.element);
                    if (self.vars.buttonAnchor) {
                        self.vars.cancelButton = shmi.createControl("iq-button", self.vars.buttonAnchor, {
                            "class-name": getButtonClassName(self),
                            "label": "${cancel}"
                        });
                        self.vars.okButton = shmi.createControl("iq-button", self.vars.buttonAnchor, {
                            "class-name": getButtonClassName(self, ["filled"]),
                            "label": "${okay}"
                        });
                    }
                }

                // LISTENERS
                self.setupListeners();

                // INTITIAL UPDATE
                self.updateCalendar();
                updateDisplayDate(self);

                // Label
                setLabelImpl(self, self.config['show-text'] ? self.config.label : null);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                initButtonListeners(self);
                self.controls.forEach((c) => c.enable());

                shmi.log("[IQ:iq-select-date] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
                self.controls.forEach((c) => c.disable());

                shmi.log("[IQ:iq-select-date] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                shmi.removeClass(self.element, 'open');
                self.vars.isOpen = false;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.controls.forEach((c) => c.lock());

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-date] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.controls.forEach((c) => c.unlock());

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-date] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this,
                    date = new Date(value * 1000),
                    oldValue = self.vars.value;

                value = parseInt(value);

                self.vars.value = value;
                self.vars.date.setFullYear(date.getYear() + 1900, date.getMonth());
                self.vars.selectedDate.setFullYear(date.getYear() + 1900, date.getMonth(), date.getDate());
                self.updateCalendar();
                updateDisplayDate(self);

                if (oldValue !== self.vars.value) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;

                return self.vars.value;
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Updates the date with the given month
             *
             * @param {int} newMonth
             * @param {object} el
             */
            updateDateMonth: function(newMonth, el) {
                var self = this;

                self.vars.date.setFullYear(self.vars.date.getYear() + 1900, newMonth);
                self.vars.selectedDate.setFullYear(self.vars.date.getYear() + 1900, newMonth, el.textContent);
            },

            /**
             * Sets up the listeners, moved here to make onInit shorter
             */
            setupListeners: function() {
                var self = this;

                // MONTH: PREVIOUS
                var previousMonthFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, event) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900, self.vars.date.getMonth() - 1);
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.previousMonthEl, previousMonthFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.previousMonthEl, previousMonthFunctions));

                // MONTH: NEXT
                var nextMonthFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, e) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900, self.vars.date.getMonth() + 1);
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.nextMonthEl, nextMonthFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.nextMonthEl, nextMonthFunctions));

                // YEAR: PREVIOUS
                var previousYearFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, e) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900 - 1, self.vars.date.getMonth());
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.previousYearEl, previousYearFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.previousYearEl, previousYearFunctions));

                // YEAR: NEXT
                var nextYearFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, e) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900 + 1, self.vars.date.getMonth());
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.nextYearEl, nextYearFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.nextYearEl, nextYearFunctions));

                // DAY
                self.vars.pressedEl = null;
                var dayFunctions = {
                    onPress: function(x, y, e) {
                        shmi.addClass(e.target, 'pressed');
                        self.vars.pressedEl = e.target;
                    },
                    onRelease: function(x, y, e) {
                        shmi.removeClass(self.vars.pressedEl, 'pressed');
                    },
                    onClick: function(x, y, e) {
                        var dayEl = e.target;
                        if (shmi.hasClass(dayEl, 'iqel-previous-month-day')) {
                            self.updateDateMonth(self.vars.date.getMonth() - 1, dayEl);
                        } else if (shmi.hasClass(dayEl, 'iqel-current-month-day')) {
                            self.updateDateMonth(self.vars.date.getMonth(), dayEl);
                        } else if (shmi.hasClass(dayEl, 'iqel-next-month-day')) {
                            self.updateDateMonth(self.vars.date.getMonth() + 1, dayEl);
                        }

                        self.updateCalendar();

                        if (!self.vars.isModal) {
                            if (self.config.item) {
                                self.imports.im.writeValue(self.config.item, self.vars.value);
                            }
                            shmi.removeClass(self.element, 'open');
                            self.vars.isOpen = false;
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.daySelectEl, dayFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.daySelectEl, dayFunctions));

                // CALENDAR SELECTOR
                shmi.removeClass(self.element, 'open');
                self.vars.isOpen = false;
                var selectedFunctions = {
                    onClick: function() {
                        if (!self.vars.isOpen) {
                            shmi.addClass(self.element, 'open');
                            self.vars.isOpen = true;
                            if (self.vars.isModal) {
                                self.vars.tempValue = self.vars.value;
                                const date = new Date(self.vars.tempValue * 1000);
                                self.vars.date.setFullYear(date.getFullYear(), date.getMonth());
                                self.vars.selectedDate.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
                                self.updateCalendar();
                            }
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.element, selectedFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.element, selectedFunctions));
            },
            /**
             * Updates the calendar element
             *
             */
            updateCalendar: function() {
                const self = this,
                    modal = self.vars.isModal,
                    oldValue = modal ? self.vars.tempValue : self.vars.value,
                    date = new Date((modal ? self.vars.tempValue : self.vars.value) * 1000);

                self.initDate();

                date.setFullYear(self.vars.selectedDate.getYear() + 1900, self.vars.selectedDate.getMonth(), self.vars.selectedDate.getDate());
                const newValue = date.getTime() / 1000;

                shmi.log("[IQ:iq-select-date] selected time: " + newValue, 0);

                var currentMonth = self.vars.date.getMonth(),
                    currentYear = self.vars.date.getYear() + 1900,
                    firstDay = new Date(currentYear, currentMonth, 1).getDay(),
                    start = firstDay - 1;

                self.vars.currentMonthEl.textContent = shmi.localize(self.config.monthnames[currentMonth]);
                self.vars.currentYearEl.textContent = currentYear;

                if (firstDay === 1) {
                    start = 7;
                } else if (firstDay === 0) {
                    start = 6;
                }

                var prevDays = self.getDaysOfMonth(currentMonth, currentYear),
                    curDays = self.getDaysOfMonth(currentMonth + 1, currentYear),
                    tempDayEl = shmi.getUiElements('day', self.element),
                    tempDay = prevDays - start + 1;

                for (var i = 0; i < start; i++) {
                    tempDayEl[i].className = 'iqel-previous-month-day iqel-calendar-day';
                    tempDayEl[i].textContent = tempDay;
                    if (new Date(currentYear, currentMonth - 1, tempDay).getTime() === self.vars.selectedDate.getTime()) {
                        shmi.addClass(tempDayEl[i], 'iqel-selected');
                    } else {
                        shmi.removeClass(tempDayEl[i], 'iqel-selected');
                    }
                    tempDay++;
                }

                tempDay = 1;
                for (i = start; i < curDays + start; i++) {
                    tempDayEl[i].className = 'iqel-current-month-day iqel-calendar-day';
                    tempDayEl[i].textContent = tempDay;
                    if (new Date(currentYear, currentMonth, tempDay).getTime() === self.vars.selectedDate.getTime()) {
                        shmi.addClass(tempDayEl[i], 'iqel-selected');
                    } else {
                        shmi.removeClass(tempDayEl[i], 'iqel-selected');
                    }
                    tempDay++;
                }

                tempDay = 1;
                for (i = curDays + start; i < tempDayEl.length; i++) {
                    tempDayEl[i].className = 'iqel-next-month-day iqel-calendar-day';
                    tempDayEl[i].textContent = tempDay;
                    if (new Date(currentYear, currentMonth + 1, tempDay).getTime() === self.vars.selectedDate.getTime()) {
                        shmi.addClass(tempDayEl[i], 'iqel-selected');
                    } else {
                        shmi.removeClass(tempDayEl[i], 'iqel-selected');
                    }
                    tempDay++;
                }

                if (!modal && (oldValue !== newValue)) {
                    self.vars.value = newValue;
                    updateDisplayDate(self);
                    self.fire("change", {
                        value: self.vars.value
                    });
                } else if (modal) {
                    self.vars.tempValue = newValue;
                }
            },

            /**
             * Returns number of days for month of year
             *
             * @param month - month of year
             * @param year - year
             * @return days days in month
             */
            getDaysOfMonth: function(month, year) {
                var d = new Date(year, month, 0);
                return d.getDate();
            },

            /**
             * Initializes the date if not set. As setValue might be called before onInit has finished we need to do it like this
             */
            initDate: function() {
                var self = this;

                // Initialize date
                if (self.vars.date === null) {
                    self.vars.date = new Date();
                }

                if (self.vars.selectedDate === null) {
                    self.vars.selectedDate = new Date(new Date().setHours(0, 0, 0, 0));
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-radio
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-radio",
 *     "name": null,
 *     "template": "default/iq-select-radio.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-select-radio", // control name in camel-case
        uiType = "iq-select-radio", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-select-radio iq-variant-01",
        "name": null,
        "template": "default/iq-select-radio.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "show-icon": true,
        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": true,
        "tooltip": null,
        "options": [],
        "selected": -1
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: -1,
            active: false,

            // DOM Elements
            optionContainerEl: null,
            optionEls: [],
            labelEl: null,
            iconEl: null,
            iconClassEl: null,
            label: null,
            selectedEl: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [
            "change"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            // Note: onTemplate has not been implemented in this version anymore as variants should be used instead

            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.optionEl = shmi.getUiElement('option', self.element); // To be duplicated!
                self.vars.optionContainerEl = shmi.getUiElement('option-container', self.element);

                // Verify options set and available in HTML template
                if (!self.config['options']) {
                    shmi.log("[IQ:iq-select-radio] No options defined in config", 1);
                    return;
                } else {
                    shmi.log("[IQ:iq-select-radio] Options newly defined", 1);
                }
                if (!self.vars.optionEl || !self.vars.optionContainerEl) {
                    shmi.log("[IQ:iq-select-radio] Missing option and/or option container element in template!", 1);
                    return;
                }

                // Remove initial option from DOM after cloning it for reuse
                const clone = self.vars.optionEl.cloneNode(true);
                self.vars.optionContainerEl.removeChild(self.vars.optionEl);
                self.vars.optionEl = clone;

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-select-radio] no button-icon element provided', 1);
                } else if (self.config['icon-src']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                } else if (self.config['icon-class']) {
                    shmi.removeClass(self.vars.iconEl, "iq-icon");
                    shmi.addClass(self.vars.iconEl, "iq-icon-class");
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    self.vars.iconEl.style.display = 'none';
                    shmi.addClass(self.element, "no-icon");
                }

                // Label
                setLabelImpl(self, self.config.label);

                self.rebuildOptions();
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-select-radio] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-select-radio] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                //self.vars.currentValueEl.blur();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(this.element, 'locked');

                shmi.log("[IQ:iq-select-radio] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-radio] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    changed = (self.vars.value !== value);

                self.vars.value = value;

                const optionIdx = self.config.options.findIndex((option) => option.value === value);
                if (optionIdx !== -1) {
                    self.setSelected(self.vars.optionEls[optionIdx]);
                } else {
                    self.setSelected(null);
                }

                shmi.log("[IQ:iq-select-radio] value set: " + value, 1);
                if (changed) {
                    this.fire('change', { value: self.vars.value });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {
                const self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * (Re-)Creates the widgets options elements.
             *
             * @param {boolean} enableListeners Whether or not to enable listeners attached to the option elements.
             */
            rebuildOptions: function(enableListeners) {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                // Remove container listeners
                self.vars.listeners.forEach(function(listener) {
                    listener.disable();
                });
                self.vars.listeners = [];

                // We're going to build the dynamic option list by cloning the option element in the template which we have removed from the DOM before
                const containerEl = self.vars.optionContainerEl,
                    optionTemplate = self.vars.optionEl;

                // Clear content
                self.vars.optionEls = [];
                containerEl.innerHTML = '';

                // Add options one-by-one to the container
                self.config.options.forEach(function(option, index) {
                    const newOption = optionTemplate.cloneNode(true);
                    newOption.dataset.value = option.value;
                    newOption.dataset.no = index;

                    const labelEl = shmi.getUiElement('option-label', newOption);
                    if (option.label && labelEl) {
                        labelEl.textContent = shmi.localize(option.label);
                    } else {
                        shmi.addClass(newOption, "no-label");
                    }

                    // Prefer icon over icon class
                    const iconEl = shmi.getUiElement('option-icon', newOption);
                    if (option['icon-src'] && iconEl) {
                        applyIcon(iconEl, option["icon-src"], null, self.config["inline-svg-icon"]);
                    } else if (option['icon-class'] && iconEl) {
                        applyIcon(iconEl, null, option["icon-class"]);
                    } else {
                        shmi.addClass(newOption, "no-icon");
                    }

                    if (option['tooltip'] && iconEl) {
                        iconEl.setAttribute("title", shmi.localize(option['tooltip']));
                    }

                    containerEl.appendChild(newOption);

                    self.vars.optionEls.push(newOption);
                });

                // Event listeners
                const eventFunctions = {
                    onPress: function onPress(x, y, e) {
                        shmi.addClass(e.currentTarget, 'pressed');
                    },

                    onRelease: function onRelease(x, y, e) {
                        self.vars.optionEls.forEach((element) => {
                            shmi.removeClass(element, 'pressed');
                        });
                    },

                    onClick: function onClick(x, y, e) {
                        const targetEl = e.target;

                        let useTargetEl = null,
                            changed = false,
                            value = null;

                        if (typeof targetEl.dataset.ui !== 'undefined' && targetEl.dataset.ui === 'option') {
                            // We already have the correct option element!
                            useTargetEl = targetEl;
                        } else {
                            // We have to traverse the tree up until we find it
                            useTargetEl = targetEl.closest('[data-ui="option"]');
                        }
                        if (!useTargetEl) {
                            console.error(self.uiType, "Could not match clicked option");
                        } else if (typeof useTargetEl.dataset.no === 'undefined') {
                            // Should not happen as we add it ourselves!
                            console.error(self.uiType, "Could not match clicked option (data-no missing!?)");
                            console.log(useTargetEl);
                        } else {
                            value = self.config.options[parseInt(useTargetEl.dataset.no)];
                            changed = (value.value !== self.vars.value);
                            self.vars.value = value.value;
                            self.setSelected(useTargetEl);
                            self.updateValue();
                            if (changed) {
                                self.fire('change', { value: self.vars.value });
                            }
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.optionContainerEl, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.optionContainerEl, eventFunctions));
                if (enableListeners) {
                    self.listeners.forEach(function(listener) {
                        listener.enable();
                    });
                }
            },

            /**
             * Sets the selected element, not called by VISUALS
             *
             * @param element - element to select
             */
            setSelected: function(element) {
                const self = this;

                self.vars.optionEls.forEach((el) => {
                    if (element !== el) {
                        shmi.removeClass(el, 'selected');
                    } else {
                        shmi.addClass(el, 'selected');
                    }
                });

                self.vars.selectedEl = element;
            },

            /**
             * @returns {number}
             */
            getCurrentIndex: function() {
                const self = this,
                    selectedEl = self.vars.selectedEl;

                return self.vars.optionEls.findIndex((element) => selectedEl === element);
            },

            /**
             * Can be called externally, not called by VISUALS
             * @param options
             */
            setOptions: function(options) {
                const self = this;

                self.config.options = options;

                // reset properties
                self.vars.value = -1;
                self.vars.selectedEl = null;

                self.rebuildOptions(self.active);
            },

            /**
             * Writes current value to connected data source
             */
            updateValue: function() {
                const self = this;
                if (self.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(this.config.item, self.vars.value);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-time
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-time",
 *     "name": null,
 *     "template": "custom/controls/iq-select-time"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "type": Type is INT
 * "auto-label": Whether to use the auto-label (from item)
 * "tooltip": Tooltip
 * "isUTC": Whether the timestamp given is UTC time (and not local time)
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-select-time", // control name in camel-case
        uiType = "iq-select-time", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-select-time",
        "name": null,
        "template": "default/iq-select-time.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "numpad-enabled": false,
        "tooltip": null,
        "isUTC": false,
        "icon-src": "pics/system/controls/iq-select-time/clock-icon.svg",
        "timeformat": "$HH:$mm:$ss"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    const selectMode = {
        HOURS: 0,
        MINUTES: 1,
        SECONDS: 2,
        0: "HOURS",
        1: "MINUTES",
        2: "SECONDS"
    };

    /**
     * update text displaying current time selection
     *
     * @param {object} self instance reference
     */
    function updateTimeString(self) {
        const { formatDateTime } = shmi.requires("visuals.tools.date");

        if (self.vars.buttonValue) {
            const timeFormat = shmi.localize(self.config.timeformat);
            self.vars.buttonValue.textContent = formatDateTime(self.vars.value, {
                datestring: (typeof timeFormat === "string") ? timeFormat : "$HH:$mm:$ss",
                utc: self.config.isUTC
            });
        }
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText, labelElement) {
        if (!labelElement) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            labelElement.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            labelElement.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * get selected hour from selection marker angle & position
     *
     * @param {number} angle marker angle
     * @param {boolean} inner marker position, `true` for inner ring, `false`outer ring
     * @return {number} selected hour
     */
    function getHour(angle, inner) {
        let hour = Math.round(angle / 30);
        if (hour === 12) {
            hour = 0;
        }
        if (inner) {
            hour += 12;
        }
        return hour;
    }

    /**
     * get selected minute or our from selection marker angle
     *
     * @param {number} angle marker angle
     * @return {number} selected hour / minute
     */
    function getMinuteOrSecond(angle) {
        const value = Math.round(angle / 6);
        return value === 60 ? 0 : value;
    }

    /**
     * update active selection mode
     *
     * @param {object} self instance reference
     * @param {string|null} [mode=null] either `"HOURS"`, `"MINUTES"` or `"SECONDS"`. `null` sets current selection mode
     */
    function updateSelectMode(self, mode = null) {
        if (typeof selectMode[mode] === "number") {
            self.vars.selectMode = mode;
        }

        switch (selectMode[self.vars.selectMode]) {
        case selectMode.HOURS:
            shmi.removeClass(self.element, "select-mode-minutes select-mode-seconds");
            shmi.addClass(self.element, "select-mode-hours");
            break;
        case selectMode.MINUTES:
            shmi.removeClass(self.element, "select-mode-hours select-mode-seconds");
            shmi.addClass(self.element, "select-mode-minutes");
            break;
        case selectMode.SECONDS:
            shmi.removeClass(self.element, "select-mode-hours select-mode-minutes");
            shmi.addClass(self.element, "select-mode-seconds");
            break;
        default:
        }
        updateClipPath(self);
    }

    /**
     * getRandomId - generates random ids for our stylecache
     *
     * @return {string} random id
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * update selection marker clip path
     *
     * @param {object} self instance reference
     * @param {number|null} [tempAngle=null] temporary selection marker angle during drag operations
     * @param {boolean} [tempOuter=false] temporary ring position during drag operations
     */
    function updateClipPath(self, tempAngle = null, tempOuter = false) {
        const { dims } = self.vars,
            path = self.vars.clipPath.path, width = dims.width, height = dims.height, r1 = dims.r1, r2 = dims.r2, connectorWidth = dims.connectorWidth,
            values = self.vars.isModal ? self.vars.tempValues : self.vars.currentValues;
        let padding = 0;
        switch (selectMode[self.vars.selectMode]) {
        case selectMode.HOURS:
            padding = (values.hours < 12) ? dims.outerOffset : dims.innerOffset;
            break;
        case selectMode.MINUTES:
        case selectMode.SECONDS:
            padding = dims.outerOffset;
            break;
        default:
        }
        if (tempAngle !== null) {
            padding = tempOuter ? dims.outerOffset : dims.innerOffset;
        }
        const connectorLength = Math.abs((height / 2) - padding - (2 * r2) - r1);
        let angle = 0;
        if (tempAngle !== null) {
            angle = tempAngle;
        } else {
            switch (selectMode[self.vars.selectMode]) {
            case selectMode.HOURS:
                angle = (values.hours % 12) * 30;
                break;
            case selectMode.MINUTES:
                angle = values.minutes * 6;
                break;
            case selectMode.SECONDS:
                angle = values.seconds * 6;
                break;
            default:
            }
        }

        path.setAttribute("d", `m${(width / 2) + (connectorWidth / 2)} ${padding + (2 * r2)}a${r2} ${r2} 0 1 0 -${connectorWidth} 0 v${connectorLength} a${r1} ${r1} 0 1 0 ${connectorWidth} 0 v-${connectorLength}z`);
        path.setAttribute("transform", `rotate(${angle}, ${width / 2}, ${height / 2})`);
    }

    /**
     * initialize clip path SVG element
     *
     * @param {object} self instance reference
     * @param {number} [width=256] SVG element width
     * @param {number} [height=256] SVG element height
     * @return {object} clip path reference object
     */
    function createClipPath(self, width = 256, height = 256) {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"),
            defs = document.createElementNS(svg.namespaceURI, "defs"),
            clipPath = document.createElementNS(svg.namespaceURI, "clipPath"),
            path = document.createElementNS(svg.namespaceURI, "path"),
            id = `select-time-clippath-${getRandomId()}`;

        svg.setAttribute("width", 0);
        svg.setAttribute("height", 0);
        clipPath.setAttribute("id", id);
        clipPath.appendChild(path);
        defs.appendChild(clipPath);
        svg.appendChild(defs);

        return {
            base: svg,
            path,
            id
        };
    }

    /**
     * set state of 'disabled' attribute for input elements
     *
     * @param {object} self instance reference
     * @param {boolean} disabled state of disabled attribute
     */
    function setInputDisabledState(self, disabled) {
        if (self.vars.hourEl) {
            self.vars.hourEl.disabled = disabled;
        }

        if (self.vars.minuteEl) {
            self.vars.minuteEl.disabled = disabled;
        }

        if (self.vars.secondEl) {
            self.vars.secondEl.disabled = disabled;
        }
    }

    /**
     * apply translation style for clockface value element
     *
     * @param {HTMLElement} element clockface value element
     * @param {object} dims clockface dimensions
     * @param {number} position element position index
     * @param {number} distance distance from edge of clockface
     */
    function setFaceValueStyle(element, dims, position, distance) {
        const x = Math.cos(1.5 * Math.PI + (position) * (Math.PI / 6)) * ((dims.width / 2) - distance),
            y = Math.sin(1.5 * Math.PI + (position) * (Math.PI / 6)) * ((dims.height / 2) - distance);

        element.style.position = "absolute";
        element.style.transform = `translate(${Math.floor(x * 100) / 100}px, ${Math.floor(y * 100) / 100}px)`;
    }

    /**
     * update clockface elements
     *
     * @param {object} self instance reference
     */
    function updateClockFace(self) {
        const { dims } = self.vars,
            outerDistance = dims.r2 + dims.outerOffset,
            innerDistance = dims.r2 + dims.innerOffset;

        self.vars.clockFaceHours.values.forEach((element, idx) => {
            setFaceValueStyle(element, dims, idx % 12, idx >= 12 ? innerDistance : outerDistance);
        });

        self.vars.clockFaceHoursOverlay.values.forEach((element, idx) => {
            setFaceValueStyle(element, dims, idx % 12, idx >= 12 ? innerDistance : outerDistance);
        });

        self.vars.clockFaceMinutes.values.forEach((element, idx) => {
            setFaceValueStyle(element, dims, idx, outerDistance);
        });

        self.vars.clockFaceMinutesOverlay.values.forEach((element, idx) => {
            setFaceValueStyle(element, dims, idx, outerDistance);
        });

        self.vars.clockFaceSeconds.values.forEach((element, idx) => {
            setFaceValueStyle(element, dims, idx, outerDistance);
        });

        self.vars.clockFaceSecondsOverlay.values.forEach((element, idx) => {
            setFaceValueStyle(element, dims, idx, outerDistance);
        });
    }

    /**
     * get angle of selection vector
     *
     * @param {number} x x-coordinate
     * @param {number} y y-coordinate
     * @return {number} selection vector angle
     */
    function getAngle(x, y) {
        return Math.round((450 + (Math.atan2(y, x) * 180) / Math.PI) % 360);
    }

    /**
     * get length of selection vector
     *
     * @param {number} x x-coordinate
     * @param {number} y y-coordinate
     * @return {number} selection vector length
     */
    function getLength(x, y) {
        return Math.sqrt(x ** 2 + y ** 2);
    }

    /**
     * get parameters of current selection.
     *
     * `rx` - x-coordinate relative to center of clockface
     * `ry` - y-coordinate relative to center of clockface
     * `angle` - selection vector angle
     * `length` - selection vector length
     *
     * @param {object} self instance reference
     * @param {object} boundingBox clockface bounding-box
     * @param {number} x input x-coordinate (clientX)
     * @param {number} y input y-coordinate (clientY)
     * @return {object} selection parameters
     */
    function getSelectionParameters(self, boundingBox, x, y) {
        const { dims } = self.vars,
            rx = x - (boundingBox.x + (dims.width / 2)),
            ry = y - (boundingBox.y + (dims.height / 2)),
            angle = getAngle(rx, ry),
            length = getLength(rx, ry);

        return {
            rx,
            ry,
            angle,
            length
        };
    }

    /**
     * update selected value depending on current selection mode
     *
     * @param {object} self instance reference
     * @param {number} angle selection vector angle
     * @param {number|null} [length=null] selection vector length
     */
    function updateSelectedValue(self, angle, length = null) {
        const { dims } = self.vars,
            values = self.vars.isModal ? self.vars.tempValues : self.vars.currentValues;

        switch (selectMode[self.vars.selectMode]) {
        case selectMode.HOURS:
            updateClipPath(self, angle, length >= ((dims.width / 2) - (dims.innerOffset + dims.r2 / 2)));
            values.hours = getHour(angle, length < ((dims.width / 2) - (dims.innerOffset + dims.r2 / 2)));
            break;
        case selectMode.MINUTES:
            updateClipPath(self, angle, true);
            values.minutes = getMinuteOrSecond(angle);
            break;
        case selectMode.SECONDS:
            updateClipPath(self, angle, true);
            values.seconds = getMinuteOrSecond(angle);
            break;
        default:
        }
    }

    /**
     * get clockface element for active selection mode
     *
     * @param {object} self instance reference
     * @return {HTMLElement|null} clockface element
     */
    function getClockFaceElement(self) {
        switch (self.vars.selectMode) {
        case "HOURS":
            return self.vars.clockFaceHours.base;
        case "MINUTES":
            return self.vars.clockFaceMinutes.base;
        case "SECONDS":
            return self.vars.clockFaceSeconds.base;
        default:
            return null;
        }
    }

    /**
     * initialize input listeners for clockfaces
     *
     * @param {object} self control instance
     */
    function setupClockfaceListeners(self) {
        let boundingBox = null,
            dragging = false;
        const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
            element = self.vars.clockFaceContainer,
            values = self.vars.isModal ? self.vars.tempValues : self.vars.currentValues,
            handler = {
                onPress: (x, y, event) => {
                    boundingBox = getClockFaceElement(self).getBoundingClientRect();
                    const { angle, length } = getSelectionParameters(self, boundingBox, x, y);
                    updateSelectedValue(self, angle, length);
                    self.updateInputFields(values);
                },
                onDrag: (dx, dy, event) => {
                    dragging = true;
                    event.preventDefault();
                    const { clientX: x, clientY: y } = event.touches ? event.touches[0] : event,
                        { angle, length } = getSelectionParameters(self, boundingBox, x, y);

                    updateSelectedValue(self, angle, length);
                    self.updateInputFields(values);
                },
                onRelease: (x, y, event) => {
                    if (dragging) {
                        dragging = false;
                        const { angle, length } = getSelectionParameters(self, boundingBox, x, y);

                        updateSelectedValue(self, angle, length);
                        self.updateValue();
                        const currentMode = selectMode[self.vars.selectMode];
                        if (currentMode < 2) {
                            updateSelectMode(self, selectMode[currentMode + 1]);
                        }
                    }
                },
                onClick: (x, y, event) => {
                    const { angle, length } = getSelectionParameters(self, boundingBox, x, y);

                    updateSelectedValue(self, Math.round(angle / 30) * 30, length);
                    self.updateValue();
                    const currentMode = selectMode[self.vars.selectMode];
                    if (currentMode < 2) {
                        updateSelectMode(self, selectMode[currentMode + 1]);
                    }
                }
            };

        self.vars.listeners.push(
            new MouseListener(element, handler, { dragThreshold: 0 }),
            new TouchListener(element, handler, true, { dragThreshold: 0 })
        );
    }

    /**
     * initialize clockface element with values
     *
     * @param {object} self instance reference
     * @param {string} [type="hours"] clockface type, either `"hours"`, `"minutes"` or `"seconds"`
     */
    function initClockFace(self, type = "hours") {
        switch (type) {
        case "hours": {
            for (let i = 0; i < 24; i++) {
                const div = document.createElement("div");
                div.classList.add("iqel-face-value");
                div.textContent = i === 0 ? "00" : `${i}`;
                self.vars.clockFaceHours.values.push(div);
                self.vars.clockFaceHours.base.appendChild(div);

                const overlayDiv = div.cloneNode(true);
                self.vars.clockFaceHoursOverlay.values.push(overlayDiv);
                self.vars.clockFaceHoursOverlay.base.appendChild(overlayDiv);
            }
            break;
        }
        case "minutes": {
            for (let i = 0; i < 12; i++) {
                const div = document.createElement("div");
                div.classList.add("iqel-face-value");
                div.textContent = i === 0 ? "00" : `${i * 5}`;
                self.vars.clockFaceMinutes.values.push(div);
                self.vars.clockFaceMinutes.base.appendChild(div);

                const overlayDiv = div.cloneNode(true);
                self.vars.clockFaceMinutesOverlay.values.push(overlayDiv);
                self.vars.clockFaceMinutesOverlay.base.appendChild(overlayDiv);
            }
            break;
        }
        case "seconds": {
            for (let i = 0; i < 12; i++) {
                const div = document.createElement("div");
                div.classList.add("iqel-face-value");
                div.textContent = i === 0 ? "00" : `${i * 5}`;
                self.vars.clockFaceSeconds.values.push(div);
                self.vars.clockFaceSeconds.base.appendChild(div);

                const overlayDiv = div.cloneNode(true);
                self.vars.clockFaceSecondsOverlay.values.push(overlayDiv);
                self.vars.clockFaceSecondsOverlay.base.appendChild(overlayDiv);
            }
            break;
        }
        default:
            console.error(`Invalid clockface type specified: ${type}`);
        }
    }

    /**
     * update widget dimensions
     *
     * @param {object} self instance reference
     */
    function updateDimensions(self) {
        const clockFaceElement = getClockFaceElement(self);
        self.vars.dims.width = clockFaceElement.clientWidth;
        self.vars.dims.height = clockFaceElement.clientHeight;
        const baseStyle = getComputedStyle(self.element);
        const outerOffset = parseInt(baseStyle.getPropertyValue("--face-outer-offset"));
        if (!isNaN(outerOffset)) {
            self.vars.dims.outerOffset = outerOffset;
        }
        const innerOffset = parseInt(baseStyle.getPropertyValue("--face-inner-offset"));
        if (!isNaN(innerOffset)) {
            self.vars.dims.innerOffset = innerOffset;
        }

        const markerOuterRadius = parseInt(baseStyle.getPropertyValue("--marker-outer-radius"));
        if (!isNaN(markerOuterRadius)) {
            self.vars.dims.r2 = markerOuterRadius;
        }

        const markerCenterRadius = parseInt(baseStyle.getPropertyValue("--marker-center-radius"));
        if (!isNaN(markerCenterRadius)) {
            self.vars.dims.r1 = markerCenterRadius;
        }

        const markerLineWidth = parseInt(baseStyle.getPropertyValue("--marker-line-width"));
        if (!isNaN(markerLineWidth)) {
            self.vars.dims.connectorWidth = markerLineWidth;
        }

        if (self.vars.clipPath === null) {
            self.vars.clipPath = createClipPath(self, self.vars.dims.width, self.vars.dims.height);
            self.vars.clipPath.base.setAttribute("viewBox", `0 0 ${self.vars.dims.width} ${self.vars.dims.height}`);
            self.vars.clockFaceContainer.appendChild(self.vars.clipPath.base);
            self.vars.clockFaceContainer.style.setProperty("--clippath-url", `url(#${self.vars.clipPath.id})`);
        } else {
            self.vars.clipPath.base.setAttribute("viewBox", `0 0 ${self.vars.dims.width} ${self.vars.dims.height}`);
        }
        updateClockFace(self);
        updateClipPath(self);
    }

    /**
     * check if current layout is in modal display mode
     *
     * @param {object} self instance reference
     * @return {boolean} `true` if modal layout is active, `false` else
     */
    function isModal(self) {
        if (self.vars.timeSelection) {
            const style = getComputedStyle(self.vars.timeSelection);
            if (["fixed", "absolute"].includes(style.position)) {
                return true;
            }
        }

        return false;
    }

    /**
     * apply selected time as current value
     *
     * @param {object} self instance reference
     */
    function applyTime(self) {
        const newValue = self.computeNewValue();

        if (self.config.item) {
            // Make sure we write a string to the item if it was a string before
            if (self.vars.itemNeedsString) {
                self.imports.im.writeValue(self.config.item, String(newValue));
            } else {
                self.imports.im.writeValue(self.config.item, newValue);
            }
        }

        // Only fire a 'changed' event if the value actually changed.
        if (self.vars.value !== newValue) {
            self.vars.value = newValue;
            if (self.vars.isModal) {
                self.vars.currentValues.hours = self.vars.tempValues.hours;
                self.vars.currentValues.minutes = self.vars.tempValues.minutes;
                self.vars.currentValues.seconds = self.vars.tempValues.seconds;
            }
            updateTimeString(self);
            self.fire('change', { value: newValue });
        }
    }

    /**
     * initialize listeners for ok-/cancel-buttons
     *
     * @param {object} self intance reference
     */
    function initButtonListeners(self) {
        if (self.vars.okButton) {
            self.vars.tokens.push(
                self.vars.okButton.listen("click", () => {
                    applyTime(self);
                    shmi.removeClass(self.element, "open");
                    self.vars.isOpen = false;
                })
            );
        }

        if (self.vars.cancelButton) {
            self.vars.tokens.push(
                self.vars.cancelButton.listen("click", () => {
                    shmi.removeClass(self.element, "open");
                    self.vars.isOpen = false;
                })
            );
        }
    }

    /**
     * get 'class-name' option value for created buttons
     *
     * @param {object} self instance reference
     * @param {string[]} [additionalSelectors=[]] additional css-selectors to include
     * @return {string} 'class-name' option value
     */
    function getButtonClassName(self, additionalSelectors = []) {
        if (self.element.classList.contains("secondary")) {
            additionalSelectors.push("secondary");
        } else if (self.element.classList.contains("tertiary")) {
            additionalSelectors.push("tertiary");
        }

        return `iq-button iq-variant-01${additionalSelectors?.length ? " " + additionalSelectors.join(" ") : ""}`;
    }

    /**
     * Adapts a DOM event listener to the visuals listener interface.
     *
     * @param {Element} domElement Element to attach the event listener to.
     * @param {string} eventName Name of the event to listen for.
     * @param {function} eventHandler Event handler function.
     *
     * @returns {{enable: function, disable: function}}
     */
    function makeDomEventListener(domElement, eventName, eventHandler) {
        return {
            enable: domElement.addEventListener.bind(domElement, eventName, eventHandler),
            disable: domElement.removeEventListener.bind(domElement, eventName, eventHandler)
        };
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            tokens: [],
            value: 0,
            active: false,
            label: null,
            subscriptionTargetId: null,

            // DOM Elements
            labelEl: null,
            hourEl: null,
            minuteEl: null,
            secondEl: null,
            currentValues: {
                hours: 0,
                minutes: 0,
                seconds: 0
            },
            tempValues: {
                hours: 0,
                minutes: 0,
                seconds: 0
            },
            hours: 0,
            minutes: 0,
            seconds: 0,
            evListeners: [],

            itemNeedsString: false,
            clipPath: null,
            dims: {
                width: null,
                height: null,
                r1: 4,
                r2: 24,
                connectorWidth: 2,
                outerOffset: 2,
                innerOffset: 32
            },
            resizeObserver: null,
            resizeListener: null,
            clockFaceContainer: null,
            clockFaceHours: {
                base: null,
                values: []
            },
            clockFaceHoursOverlay: {
                base: null,
                values: []
            },
            clockFaceMinutes: {
                base: null,
                values: []
            },
            clockFaceMinutesOverlay: {
                base: null,
                values: []
            },
            clockFaceSeconds: {
                base: null,
                values: []
            },
            clockFaceSecondsOverlay: {
                base: null,
                values: []
            },
            selectMode: "HOURS",
            isOpen: false,
            timeSelection: null,
            buttonElement: null,
            buttonLabel: null,
            buttonIcon: null,
            buttonValue: null,
            isModal: false,
            buttonAnchor: null,
            okButton: null,
            cancelButton: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);

                self.vars.clockFaceContainer = shmi.getUiElement("clockface-container", self.element);

                self.vars.clockFaceHours.base = shmi.getUiElement("clockface-hours", self.element);
                self.vars.clockFaceHoursOverlay.base = shmi.getUiElement("clockface-hours-overlay", self.element);

                self.vars.clockFaceMinutes.base = shmi.getUiElement("clockface-minutes", self.element);
                self.vars.clockFaceMinutesOverlay.base = shmi.getUiElement("clockface-minutes-overlay", self.element);

                self.vars.clockFaceSeconds.base = shmi.getUiElement("clockface-seconds", self.element);
                self.vars.clockFaceSecondsOverlay.base = shmi.getUiElement("clockface-seconds-overlay", self.element);

                self.vars.timeSelection = shmi.getUiElement("time-selection", self.element);
                self.vars.buttonElement = shmi.getUiElement("selected-time", self.element);
                self.vars.buttonLabel = shmi.getUiElement("button-label", self.element);
                self.vars.buttonIcon = shmi.getUiElement("button-icon", self.element);
                self.vars.buttonValue = shmi.getUiElement("button-value", self.element);
                self.vars.buttonAnchor = shmi.getUiElement("button-anchor", self.element);

                if (!self.vars.buttonIcon) {
                    shmi.log('[IQ:iq-select-date] no button-icon element provided', 1);
                } else if (self.config['icon-src']) {
                    applyIcon(self.vars.buttonIcon, self.config["icon-src"], null, true);
                } else {
                    shmi.addClass(self.element, 'no-icon');
                }

                self.setupInputComponent({
                    varName: "hourEl",
                    uiElementName: 'hour-select',
                    inputValidator: self.validateGeneric.bind(this, /^[0-1]?[0-9]$|^2[0-3]$/, 'hours'),
                    max: 23,
                    label: '${V_HOURS}'
                }, "HOURS");

                self.setupInputComponent({
                    varName: "minuteEl",
                    uiElementName: 'minute-select',
                    inputValidator: self.validateGeneric.bind(this, /^[0-5]?[0-9]$/, 'minutes'),
                    max: 59,
                    label: '${V_MINUTES}'
                }, "MINUTES");

                self.setupInputComponent({
                    varName: "secondEl",
                    uiElementName: 'second-select',
                    inputValidator: self.validateGeneric.bind(this, /^[0-5]?[0-9]$/, 'seconds'),
                    max: 59,
                    label: '${V_SECONDS}'
                }, "SECONDS");

                self.updateInputFields(self.vars.currentValues);
                updateTimeString(self);

                // Label
                setLabelImpl(self, self.config.label, self.vars.labelEl);
                setLabelImpl(self, self.config.label, self.vars.buttonLabel);

                if (self.vars.buttonElement) {
                    const handler = {
                        onClick: function() {
                            if (!self.vars.isOpen) {
                                if (self.vars.isModal) {
                                    self.vars.tempValues.hours = self.vars.currentValues.hours;
                                    self.vars.tempValues.minutes = self.vars.currentValues.minutes;
                                    self.vars.tempValues.seconds = self.vars.currentValues.seconds;
                                    self.updateInputFields(self.vars.tempValues);
                                    updateClipPath(self);
                                }
                                shmi.addClass(self.element, 'open');
                                self.vars.isOpen = true;
                                updateSelectMode(self, "HOURS");
                            }
                        }
                    };
                    self.vars.listeners.push(new MouseListener(self.vars.buttonElement, handler));
                    self.vars.listeners.push(new TouchListener(self.vars.buttonElement, handler));
                }

                initClockFace(self, "hours");
                initClockFace(self, "minutes");
                initClockFace(self, "seconds");

                self.vars.resizeObserver = new ResizeObserver(updateDimensions.bind(null, self));

                // Register resize handler for window. Workaround for detecting style changes.
                self.vars.resizeListener = makeDomEventListener(window, "resize", () => {
                    updateDimensions(self);
                });

                self.vars.isModal = isModal(self);
                if (self.vars.isModal && self.vars.buttonAnchor) {
                    self.vars.cancelButton = shmi.createControl("iq-button", self.vars.buttonAnchor, {
                        "class-name": getButtonClassName(self),
                        "label": "${cancel}"
                    });
                    self.vars.okButton = shmi.createControl("iq-button", self.vars.buttonAnchor, {
                        "class-name": getButtonClassName(self, ["filled"]),
                        "label": "${okay}"
                    });
                }
                setupClockfaceListeners(self);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.vars.resizeListener?.enable();

                setInputDisabledState(self, false);

                updateDimensions(self);
                updateSelectMode(self);

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.resizeObserver.observe(self.vars.clockFaceContainer);

                initButtonListeners(self);
                self.controls.forEach((c) => c.enable());

                self.log('Enabled', 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                shmi.removeClass(self.element, "open");
                self.vars.isOpen = false;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.vars.resizeListener?.disable();

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }
                self.vars.resizeObserver.unobserve(self.vars.clockFaceContainer);
                setInputDisabledState(self, true);

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
                self.controls.forEach((c) => c.disable());

                self.log('Disabled', 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                shmi.removeClass(self.element, "open");
                self.vars.isOpen = false;

                setInputDisabledState(self, true);

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.controls.forEach((c) => c.lock());

                shmi.addClass(self.element, 'locked');

                self.log('Locked', 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                setInputDisabledState(self, false);

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.controls.forEach((c) => c.unlock());

                shmi.removeClass(self.element, 'locked');

                self.log('Unlocked', 1);
            },

            onDelete: function() {
                var self = this;

                self.vars.evListeners.forEach(function(info) {
                    info.element.removeEventListener(info.eventType, info.listener);
                });
                self.vars.evListeners = [];
                self.vars.listeners = [];
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;

                value = parseInt(value);
                var date = new Date(value * 1000);

                self.vars.itemNeedsString = (typeof (value) === 'string');

                if (self.config.isUTC) {
                    self.vars.currentValues.hours = date.getUTCHours();
                    self.vars.currentValues.minutes = date.getUTCMinutes();
                    self.vars.currentValues.seconds = date.getUTCSeconds();
                } else {
                    self.vars.currentValues.hours = date.getHours();
                    self.vars.currentValues.minutes = date.getMinutes();
                    self.vars.currentValues.seconds = date.getSeconds();
                }

                if (!self.vars.isModal) {
                    self.updateInputFields(self.vars.currentValues);
                    updateClipPath(self);
                }

                if (self.vars.value !== value) {
                    self.vars.value = value;
                    updateTimeString(self);
                    self.fire('change', { value: value });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;

                return self.vars.value;
            },

            /** Returns the number of hours. */
            getHours: function() {
                var self = this;

                return self.vars.currentValues.hours;
            },

            /** Returns the number of minutes. */
            getMinutes: function() {
                var self = this;

                return self.vars.currentValues.minutes;
            },

            /** Returns the number of seconds. */
            getSeconds: function() {
                var self = this;

                return self.vars.currentValues.seconds;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {

            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText, self.vars.labelEl);
                    setLabelImpl(self, labelText, self.vars.buttonLabel);
                }
            },

            log: function(msg, level) {
                shmi.log('[IQ:iq-select-time] '+msg, level);
            },

            getType: function(v) {
                if (Array.isArray(v)) {
                    return 'array';
                } else if (v === null) {
                    return 'null';
                } else {
                    return typeof (v);
                }
            },

            /**
             * Checks whether or not the given value is of one of the expected types.
             *
             * @param {*} value Value to check
             * @param {string|string[]} expected Expected type or types of the provided value.
             */
            checkType: function(value, expected) {
                var self = this;

                if (Array.isArray(expected)) {
                    if (!(value in expected)) {
                        return false;
                    }
                } else if (expected !== self.getType(value)) {
                    return false;
                }

                return true;
            },

            /**
             * Throws an exception if the given value is not of the provided type.
             *
             * @param {*} value Value to check
             * @param {string|string[]} expected Expected type or types of the provided value.
             * @param {string} [message] Error message
             *
             * @throws {TypeError}
             */
            assertType: function(value, expected, message) {
                var self = this;

                if (!self.checkType(value, expected)) {
                    throw TypeError(message);
                }
            },

            /**
             * Compute new control value from the controls current state.
             */
            computeNewValue: function() {
                const self = this,
                    values = self.vars.isModal ? self.vars.tempValues : self.vars.currentValues,
                    date = new Date(self.vars.value * 1000);

                if (self.config.isUTC) {
                    date.setUTCHours(values.hours);
                    date.setUTCMinutes(values.minutes);
                    date.setUTCSeconds(values.seconds);
                } else {
                    date.setHours(values.hours);
                    date.setMinutes(values.minutes);
                    date.setSeconds(values.seconds);
                }

                return date.getTime() / 1000;
            },

            /**
             * Compute the controls new value, update its items' value and fire a
             * `change` event if the value actually changed.
             */
            updateValue: function() {
                const self = this;

                if (!self.vars.isModal) {
                    applyTime(self);
                }

                self.updateInputFields(self.vars.isModal ? self.vars.tempValues : self.vars.currentValues);
                updateClipPath(self);
            },

            /**
             * Validates and stores the nodes value. If the nodes value is invalid, it
             * is overwritten with the last known-to-be-good value.
             *
             * @param {RegExp} regex A regex that has to match the elements textContent
             * @param {string} timecomponent Component to verify. Can be 'seconds', 'minutes' or 'hours'
             * @param {Node} element Node of the input field to validate
             */
            validateGeneric: function(regex, timecomponent, element) {
                const self = this,
                    values = self.vars.isModal ? self.vars.tempValues : self.vars.currentValues;

                if (RegExp(regex).test(element.value)) {
                    values[timecomponent] = parseInt(element.value);
                    self.updateValue();
                    if (values[timecomponent] < 10 && element.value.length === 1) {
                        element.value = '0' + element.value;
                    }
                } else if (values[timecomponent] < 10) {
                    element.value = '0' + values[timecomponent];
                } else {
                    element.value = values[timecomponent];
                }
            },

            /**
             * Keypress listener for h/m/s input fields.
             *
             * @param {Node} element Node the listener is attached to
             * @param {Event} event The event
             */
            inputKeypressListener: function(element, event) {
                if (Number(event.keyCode) === 13) {
                    event.preventDefault();
                    window.getSelection().removeAllRanges();
                    element.blur();
                }
            },

            /**
             * Keyup listener for h/m/s input fields.
             *
             * @param {Node} element Node the listener is attached to
             * @param {Event} event The event
             */
            inputChangeListener: function(element, event) {
                if (Number(event.keyCode) === 13) {
                    // Don't handle enter/return key
                } else if (element.value.length > 1) {
                    window.getSelection().removeAllRanges();
                    element.blur();
                }
            },

            /**
             * Blur listener for h/m/s input fields.
             *
             * @param {Node} element Node the listener is attached to
             * @param {function} validateFunc
             * @param {Event} event The event
             */
            inputBlurListener: function(element, validateFunc, event) {
                window.getSelection().removeAllRanges();
                shmi.removeClass(element, 'selectableText');
                validateFunc(element);
            },

            /**
             * Attaches mouse and touch listeners to a given node. The listeners
             * references are stored in the controls variable store and are enabled
             * or disabled by the control.
             *
             * @param {object} vars Reference to the controls variable storage
             * @param {Node} element Element to attach the listeners to
             * @param {Function} functions Listener callback
             */
            addClickTouchListener: function(vars, element, functions) {
                vars.listeners.push(new shmi.visuals.io.MouseListener(element, functions));
                vars.listeners.push(new shmi.visuals.io.TouchListener(element, functions, true));
            },

            /**
             * Attach an event listener to a given node. The listeners are detached
             * when the control is destroyed.
             *
             * @param {object} vars Reference to the controls variable storage
             * @param {Node} element Element to attach the event listener to
             * @param {string} eventType Type of the event to attach the listener to.
             * @param {Function} listener Listener callback
             */
            addEventListener: function(vars, element, eventType, listener) {
                element.addEventListener(eventType, listener);

                vars.evListeners.push({
                    element: element,
                    eventType: eventType,
                    listener: listener
                });
            },

            /**
             * Creates input field on specified elemnt
             *
             * @param {object} vars Reference to the controls variable storage
             * @param {Node} element Element for input field
             * @param {Function} validateFunc Validation function
             * @param {object} numpadConfig Numpad configuration
             */
            createInputField: function(vars, element, validateFunc, numpadConfig, inputSelectMode) {
                var self = this;

                if (!element) {
                    self.log('no base element provided', 1);
                    return;
                }

                var appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                numpadConfig = shmi.cloneObject(numpadConfig);
                numpadConfig.callback = function(res) {
                    element.value = res;
                    validateFunc(element);
                };

                self.addClickTouchListener(vars, element, {
                    onPress: (x, y, event) => {
                        if (self.vars.selectMode !== inputSelectMode) {
                            event.preventDefault();
                            if (document.activeElement) {
                                document.activeElement.blur();
                            }
                            return;
                        }
                        if (numpadConfig.numpadEnabled || keyboardEnabled) {
                            event.preventDefault();
                            event.stopPropagation();
                        }
                    },
                    onClick: function onClick(x, y, event) {
                        if (self.vars.selectMode !== inputSelectMode) {
                            event.preventDefault();
                            updateSelectMode(self, inputSelectMode);
                            return;
                        }
                        if (numpadConfig.numpadEnabled || keyboardEnabled) {
                            numpadConfig.value = element.value;
                            shmi.numpad(numpadConfig);
                        } else {
                            element.focus();
                        }
                    }
                });

                self.addEventListener(vars, element, 'keypress', self.inputKeypressListener.bind(null, element));
                self.addEventListener(vars, element, 'input', self.inputChangeListener.bind(null, element));
                self.addEventListener(vars, element, 'blur', self.inputBlurListener.bind(null, element, validateFunc));
                self.addEventListener(vars, element, 'focus', (event) => {
                    updateSelectMode(self, inputSelectMode);
                    if (numpadConfig.numpadEnabled || keyboardEnabled) {
                        numpadConfig.value = element.value;
                        shmi.numpad(numpadConfig);
                    } else {
                        element.select();
                    }
                });
            },

            /**
             * Adds listeners to a "rocker component" (+/- buttons and input field)
             *
             * @param {object} configuration Rocker component configuration
             */
            setupInputComponent: function(configuration, inputSelectMode) {
                var self = this;

                var elementComponent = shmi.getUiElement(configuration.uiElementName, self.element);
                if (!elementComponent) {
                    self.log('Element not found: ' + configuration.uiElementName, 1);
                    return false;
                }

                var elementCurrent = shmi.getUiElement('current', elementComponent);
                if (!elementCurrent) {
                    self.log('No UI element found for ' + configuration.uiElementName + '.current', 1);
                    return false;
                }

                self.createInputField(
                    self.vars,
                    elementCurrent,
                    configuration.inputValidator.bind(self),
                    {
                        min: 0,
                        max: configuration.max,
                        label: configuration.label,
                        numpadEnabled: self.config["numpad-enabled"] || false
                    },
                    inputSelectMode
                );

                self.vars[configuration.varName] = elementCurrent;

                return true;
            },

            updateInputFields: function(values) {
                var self = this;

                if (self.vars.hourEl) {
                    self.vars.hourEl.value = (values.hours > 9) ? values.hours : ('0' + values.hours);
                }
                if (self.vars.minuteEl) {
                    self.vars.minuteEl.value = (values.minutes > 9) ? values.minutes : ('0' + values.minutes);
                }
                if (self.vars.secondEl) {
                    self.vars.secondEl.value = (values.seconds > 9) ? values.seconds : ('0' + values.seconds);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals shape widget.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-shape",
 *     "name": null,
 *     "template": "default/iq-shape",
 *     "polygons": [],
 *     "rotation": 0,
 *     "scale": 1
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * polygons {number[][]}: Array of 2d vectors used as polygons. Must contain at least 3 vectors in polygon mode.
 * rotation {number}: Rotation in degrees to apply to the shape.
 * scale {number}: Scaling factor to apply to the shape.
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqShape", // control name in camel-case
        uiType = "iq-shape", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    const defConfig = {
        "class-name": "iq-shape",
        "name": null,
        "template": "default/iq-shape",
        "polygons": [],
        "rotation": 0,
        "scale": 1
    };

    // declare private functions - START

    /**
     * Calculates a rotation matrix for a given angle.
     *
     * @param {number} rotation Rotation angle in radiants
     * @returns {number[][]} 2D rotation matrix for the given angle as array of arrays
     */
    function calculateRotationMatrix2D(rotation) {
        return [
            [Math.cos(rotation), -Math.sin(rotation)],
            [Math.sin(rotation), Math.cos(rotation)]
        ];
    }

    /**
     * Returns the sum of two 2D vectors.
     *
     * @param {number[]} vector
     * @param {number[]} translate
     * @returns {number[][]}
     */
    function translateVector2D(vector, translate) {
        return [vector[0] + translate[0], vector[1] + translate[1]];
    }

    /**
     * Returns the result of the matrix multiplication of M x V.
     *
     * @param {number[]} vector
     * @param {number[][]} matrix
     * @returns {number[]}
     */
    function rotateVector2D(vector, matrix) {
        return [
            matrix[0][0] * vector[0] + matrix[0][1] * vector[1],
            matrix[1][0] * vector[0] + matrix[1][1] * vector[1]
        ];
    }

    /**
     * Rotates a vector using [0.5, 0.5] as origin.
     *
     * @param {number[]} vector
     * @param {number[][]} matrix
     * @returns {number[]}
     */
    function rotateVector2DCenter(vector, matrix) {
        return translateVector2D(
            rotateVector2D(
                translateVector2D(vector, [-0.5, -0.5]),
                matrix
            ),
            [0.5, 0.5]
        );
    }

    function scaleVector2D(vector, scale) {
        return [vector[0] * scale, vector[1] * scale];
    }

    /**
     * Scales a vector using [0.5, 0.5] as origin.
     *
     * @param {number[]} vector
     * @param {number[][]} matrix
     * @returns {number[]}
     */
    function scaleVector2DCentered(vector, scale) {
        return translateVector2D(
            scaleVector2D(
                translateVector2D(vector, [-0.5, -0.5]),
                scale
            ),
            [0.5, 0.5]
        );
    }

    /**
     * Creates a point-string as understood by the <polygon>-tag from the given
     * polygons.
     *
     * @param {number} width Width of the SVG
     * @param {number} height Height of the SVG
     * @param {number[][]} polygons Polygons
     * @param {number} strokeWidth
     * @returns {string}
     */
    function makePolygonPoints(width, height, polygons) {
        return polygons.map(([x, y]) => `${x * width},${y * height}`).join(" ");
    }

    /**
     * Returns an object with elements.
     *
     * @param {string[]} elements UI Elements to get.
     * @param {Element} baseElement Root node for the search.
     * @returns {Object}
     */
    function getUiElements(elements, baseElement) {
        const obj = {};
        const allElementsPresent = elements.every((elementName) => {
            const element = shmi.getUiElement(elementName, baseElement);
            if (!element) {
                console.error(`[${className}] "${elementName}" element is missing in template.`);
                return false;
            }

            obj[elementName] = element;
            return true;
        });

        return allElementsPresent ? obj : null;
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {},
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const svgElement = shmi.getUiElement("shape", this.element),
                    rotMatrix = calculateRotationMatrix2D((this.config.rotation || 0) / 360 * 2 * Math.PI);

                if (!svgElement) {
                    console.error(`[${className}] "shape" element is missing in template.`);
                    return;
                }

                const elements = getUiElements(["shape-polygon", "clip-path", "clip-path-polygon"], svgElement);
                if (!elements) {
                    return;
                }

                // Setup "unique" IDs for the svg.
                const randomId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
                Object.entries(elements).forEach(([key, el]) => el.setAttribute("id", `${key}-${randomId}`));
                elements["shape-polygon"].setAttribute("clip-path", `url(#clip-path-${randomId})`);

                // Compute polygons.
                const polygons = (this.config.polygons || []).map((poly) => scaleVector2DCentered(rotateVector2DCenter(poly, rotMatrix), this.config.scale / 100)),
                    points = makePolygonPoints(svgElement.viewBox.baseVal.width, svgElement.viewBox.baseVal.height, polygons);

                // Set attributes.
                switch (elements["shape-polygon"].tagName) {
                case "polygon":
                    elements["shape-polygon"].setAttribute("points", points);
                    elements["clip-path-polygon"].setAttribute("points", points);
                    break;
                default:
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals iq-slider control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-slider",
 *     "name": null,
 *     "template": "default/iq-slider.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqSlider", // control name in camel-case
        uiType = "iq-slider", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    const defConfig = {
            "class-name": `${uiType} iq-variant-01`,
            "name": null,
            "template": "default/iq-slider.iq-variant-01",
            "send-interval": 100,
            "animation-duration": 100,
            "continuous": true,
            "min": 0,
            "max": 100,
            "step": 0,
            "precision": -1,
            "type": shmi.c("TYPE_FLOAT"),
            "label": "[Label]",
            "unit-text": "[Unit]",
            "decimal-delimiter": "${DECIMAL_DELIMITER}",
            "auto-min": true,
            "auto-max": true,
            "auto-step": true,
            "auto-label": true,
            "auto-unit-text": true,
            "auto-type": true,
            "auto-precision": true,
            "initial-text": "-",
            "show-text": true,
            "show-value-popup": true,
            "show-icon": true,
            "icon-src": null,
            "icon-class": null,
            "inline-svg-icon": true,
            "inverted": false
        },
        cssClasses = {
            NO_LABEL: "no-label",
            NO_VALUE_POPUP: "no-value",
            NO_ICON: "no-icon",
            NO_UNIT: "no-unit",
            LOCKED: "locked",
            PRESSED: "pressed",
            INVERTED: "invert-slider"
        },
        orientations = {
            VERTICAL: "vertical",
            HORIZONTAL: "horizontal"
        };

    // declare private functions - START

    /**
     * setLabel - set label text for widget. applies "no-label" CSS class when no text is specified
     *
     * @param {object} self control instance
     * @param {string} [value] text value
     */
    function setLabel(self, value) {
        const { elements } = self.vars;

        if (typeof value === "string" && value.length) {
            elements.label.textContent = shmi.localize(value);
            shmi.removeClass(self.element, cssClasses.NO_LABEL);
        } else {
            elements.label.textContent = "";
            shmi.addClass(self.element, cssClasses.NO_LABEL);
        }
    }

    /**
     * setValuePopup - enable/disable the value popup for widget. applies "no-value" CSS class when Checkbox is unchecked
     *
     * @param {object} self control instance
     * @param {string} [value] checkbox value
     */
    function setValuePopup(self, value) {
        if (value === true) {
            shmi.removeClass(self.element, cssClasses.NO_VALUE_POPUP);
        } else {
            shmi.addClass(self.element, cssClasses.NO_VALUE_POPUP);
        }
    }

    /**
     * setUnit - set unit text for widget. applies "no-unit" CSS class when no text is specified
     *
     * @param {object} self control instance
     * @param {string} [value] text value
     */
    function setUnit(self, value) {
        const { elements } = self.vars;

        if (typeof value === "string" && value.length) {
            elements.unit.textContent = shmi.localize(value);
            shmi.removeClass(self.element, cssClasses.NO_UNIT);
        } else {
            elements.unit.textContent = "";
            shmi.addClass(self.element, cssClasses.NO_UNIT);
        }
    }

    /**
     * initValuePopup - initialize value popup element according to widget configuration
     *
     * @param {object} self control instance
     */
    function initValuePopup(self) {
        const { elements } = self.vars,
            config = self.getConfig();

        if (!elements.value || (elements.value && config["show-value-popup"] === false)) {
            shmi.addClass(self.element, cssClasses.NO_VALUE_POPUP);
        }
    }

    /**
     * initIcon - initialize icon element according to widget configuration
     *
     * @param {object} self control instance
     */
    function initIcon(self) {
        const { elements } = self.vars,
            config = self.getConfig(),
            { applyIcon } = shmi.requires("visuals.tools.icons");

        if (!elements.icon) {
            shmi.addClass(self.element, cssClasses.NO_ICON);
        } else if (config["show-icon"] && config["icon-src"]) {
            applyIcon(elements.icon, config["icon-src"], null, config["inline-svg-icon"]);
        } else if (config["show-icon"] && config["icon-class"]) {
            applyIcon(elements.icon, null, config["icon-class"]);
        } else {
            shmi.addClass(self.element, cssClasses.NO_ICON);
        }
    }

    /**
     * updateDimensions - recalculate widget dimensions
     *
     * @param {object} self control instance
     */
    function updateDimensions(self) {
        const { dims, elements, orientation } = self.vars;

        switch (orientation) {
        case orientations.HORIZONTAL:
            dims.track = elements.track.clientWidth;
            dims.handle = elements.handle.offsetWidth;
            dims.touchzone = elements.touchzone.offsetWidth;
            break;
        case orientations.VERTICAL:
            dims.track = elements.track.clientHeight;
            dims.handle = elements.handle.offsetHeight;
            dims.touchzone = elements.touchzone.offsetHeight;
            break;
        default:
        }

        updateHandle(self, getTranslation(self, self.getValue()));
    }

    /**
     * Identifies the input position from a mouse or touch event
     * @param {MouseEvent | TouchEvent} event input event
     * @returns x and y position of the input
     */
    function identifyInputPos(event) {
        if (event.touches) {
            return {
                x: event.touches[0].clientX,
                y: event.touches[0].clientY
            };
        }
        return {
            x: event.clientX,
            y: event.clientY
        };
    }

    /**
     * getInputHandler - create handler for Mouse-/Touch-Listeners
     *
     * @param {object} self control instance
     * @returns {object} input handler
     */
    function getInputHandler(self) {
        let startPosX,
            startPosY,
            startHandlerX,
            startHandlerY;
        return {
            onPress: function(x, y) {
                const { handleTranslation } = self.vars;
                self.vars.dragging = false;
                shmi.addClass(self.vars.elements.handle, cssClasses.PRESSED);
                startPosX = x;
                startPosY = y;
                startHandlerX = handleTranslation.tx;
                startHandlerY = handleTranslation.ty;
            },
            onDrag: function(dx, dy, event) {
                event.preventDefault();

                const { handleTranslation, dims } = self.vars;
                const { x, y } = identifyInputPos(event);

                if (!self.vars.dragging) {
                    self.vars.dragging = true;
                    if (self.config.continuous !== false) {
                        startSend(self);
                    }
                }
                if (self.vars.orientation === orientations.VERTICAL) {
                    const updatedTy = startHandlerY + (y - startPosY);
                    if (updatedTy > 0) {
                        handleTranslation.ty = 0;
                    } else if (updatedTy < Math.min(0, dims.handle - dims.track)) {
                        handleTranslation.ty = Math.min(0, dims.handle - dims.track);
                    } else {
                        handleTranslation.ty = updatedTy;
                    }
                    updateScale(self, handleTranslation.ty);
                    updateTouchZone(self, handleTranslation.ty);
                } else {
                    const updatedTx = startHandlerX + (x - startPosX);
                    if (updatedTx < 0) {
                        handleTranslation.tx = 0;
                    } else if (updatedTx + dims.handle > dims.track) {
                        handleTranslation.tx = Math.max(0, dims.track - dims.handle);
                    } else {
                        handleTranslation.tx = updatedTx;
                    }
                    updateScale(self, handleTranslation.tx);
                    updateTouchZone(self, handleTranslation.tx);
                }
            },
            onRelease: function() {
                self.vars.dragging = false;
                stopSend(self);
                updateValue(self);
                shmi.removeClass(self.vars.elements.handle, cssClasses.PRESSED);
            }
        };
    }

    /**
     * getInputHandler - create handler for Mouse-/Touch-Listeners for the slider track.
     *
     * @param {object} self control instance
     * @returns {object} input handler
     */
    function getSliderTrackInputHandler(self) {
        const handler = getInputHandler(self),
            originalOnPress = handler.onPress,
            originalOnRelease = handler.onRelease;

        return Object.assign(handler, {
            onPress: function onPress(startX, startY, event) {
                const { handleTranslation, dims } = self.vars,
                    { min, max } = self.vars.valueSettings;

                // Prevent animations
                self.vars.dragging = true;

                // Prevents pointer events for the actual handle and applies
                // its hover style.
                shmi.addClass(self.vars.elements.track, cssClasses.PRESSED);

                // Set handle position
                const boundingBox = self.vars.elements.track.getBoundingClientRect();
                if (self.vars.orientation === orientations.VERTICAL) {
                    const posY = startY - boundingBox.y;
                    if (self.config.inverted) {
                        handleTranslation.ty = getTranslation(self, posY / dims.track * (max - min) + min);
                    } else {
                        handleTranslation.ty = getTranslation(self, (1 - posY / dims.track) * (max - min) + min);
                    }
                    updateScale(self, handleTranslation.ty);
                    updateTouchZone(self, handleTranslation.ty);
                } else {
                    const posX = startX - boundingBox.x;
                    if (self.config.inverted) {
                        handleTranslation.tx = getTranslation(self, (1 - posX / dims.track) * (max - min) + min);
                    } else {
                        handleTranslation.tx = getTranslation(self, posX / dims.track * (max - min) + min);
                    }
                    updateScale(self, handleTranslation.tx);
                    updateTouchZone(self, handleTranslation.tx);
                }

                // Update widget
                updateValue(self);

                // Call original onPress
                originalOnPress.call(handler, startX, startY, event);
            },

            onRelease: function onRelease(...args) {
                // Call original onRelease
                originalOnRelease.call(handler, ...args);

                // Remove fake hover style and enable pointer events on the
                // handle again.
                shmi.removeClass(self.vars.elements.track, cssClasses.PRESSED);

                // Focus handle
            }
        });
    }

    /**
     * startSend - start sending value updates while slider handle is dragging
     *
     * @param {object} self control instance
     */
    function startSend(self) {
        if (self.vars.sendInterval === 0) {
            self.vars.sendInterval = setInterval(updateValue.bind(null, self), self.getConfig()["send-interval"]);
        }
    }

    /**
     * stopSend - stop sending value updates when slider handle stops dragging
     *
     * @param {object} self control instance
     */
    function stopSend(self) {
        if (self.vars.sendInterval !== 0) {
            clearInterval(self.vars.sendInterval);
            self.vars.sendInterval = 0;
        }
    }

    /**
     * updateValue - update widget value after handle is moved
     *
     * @param {object} self control instance
     */
    function updateValue(self) {
        const { nv, im } = self.imports,
            { valueSettings, orientation, handleTranslation, dims, elements } = self.vars,
            { min, max } = valueSettings,
            config = self.getConfig(),
            lastValue = self.getValue(),
            ds = (orientation === orientations.VERTICAL) ? -handleTranslation.ty : handleTranslation.tx,
            maxTranslation = dims.track - dims.handle;

        let value = null;

        if (maxTranslation <= 0) {
            return; /* value cannot be calculated when handle is not movable */
        } else if (max === min) {
            value = max;
        } else if (self.config.inverted) {
            value = (1 - ds / maxTranslation) * (max - min) + min;
        } else {
            value = (ds * (max - min) / maxTranslation) + min;
        }

        if (valueSettings.type === shmi.Constants.TYPE_INT) {
            value = Math.round(value);
        }

        value = nv.applyInputSettings(value, self);

        if (value !== lastValue) {
            if (elements.value) {
                elements.value.textContent = nv.formatOutput(value, self);
            }
            self.vars.value = value;
            if (config.item) {
                im.writeValue(config.item, value);
            } else {
                self.fire("change", {
                    value: value
                });
            }
        }
    }

    /**
     * updateHandle - update handle position to match current value
     *
     * @param {object} self control instance
     * @param {number} translation handle translation
     * @returns
     */
    function updateHandle(self, translation) {
        if (isNaN(translation)) {
            return;
        }

        if (self.vars.orientation === orientations.VERTICAL) {
            self.vars.handleTranslation.ty = translation;
        } else {
            self.vars.handleTranslation.tx = translation;
        }
        updateScale(self, translation);
        updateTouchZone(self, translation);
    }

    /**
     * updateScale - update value scale to match current value
     *
     * @param {object} self control instance
     * @param {number} translation handle translation
     */
    function updateScale(self, translation) {
        const trackLength = self.vars.dims.track,
            ratio = (trackLength > 0) ? Math.abs(translation) / trackLength : 1,
            { track } = self.vars.elements;

        track.style.setProperty("--internal-fill-level", `${ratio * 100}%`);
    }

    /**
     * updateTouchZone - update touch zone element to remain within widget container boundaries
     *
     * @param {object} self
     * @param {number} translation handle translation
     */
    function updateTouchZone(self, translation) {
        const { MIN_MOVED_PX } = shmi.Constants,
            { dims, orientation, elements } = self.vars,
            maxTranslation = Math.max(0, dims.track - dims.handle);

        shmi.caf(self.vars.touchzoneRafId);
        self.vars.touchzoneRafId = shmi.raf(() => {
            translation = Math.abs(translation);

            if (orientation === orientations.VERTICAL) {
                if (translation < MIN_MOVED_PX) {
                    elements.touchzone.style.height = `calc(100% + ${Math.floor(translation) + MIN_MOVED_PX}px)`;
                    if (self.vars.touchzoneState.offsetModified) {
                        elements.touchzone.style.top = "";
                        self.vars.touchzoneState.offsetModified = false;
                    }
                    self.vars.touchzoneState.lengthModified = true;
                } else if (translation > maxTranslation - MIN_MOVED_PX) {
                    elements.touchzone.style.height = `calc(100% + ${Math.floor(maxTranslation - translation) + MIN_MOVED_PX}px)`;
                    elements.touchzone.style.top = `-${Math.floor(maxTranslation - translation)}px`;
                    self.vars.touchzoneState.lengthModified = true;
                    self.vars.touchzoneState.offsetModified = true;
                } else {
                    if (self.vars.touchzoneState.lengthModified) {
                        elements.touchzone.style.height = "";
                        self.vars.touchzoneState.lengthModified = false;
                    }
                    if (self.vars.touchzoneState.offsetModified) {
                        elements.touchzone.style.top = "";
                        self.vars.touchzoneState.offsetModified = false;
                    }
                }
            } else if (translation < MIN_MOVED_PX) {
                elements.touchzone.style.width = `calc(100% + ${Math.floor(translation) + MIN_MOVED_PX}px)`;
                elements.touchzone.style.left = `-${Math.floor(translation)}px`;
                self.vars.touchzoneState.lengthModified = true;
                self.vars.touchzoneState.offsetModified = true;
            } else if (translation > maxTranslation - MIN_MOVED_PX) {
                elements.touchzone.style.width = `calc(100% + ${Math.floor(maxTranslation - translation) + MIN_MOVED_PX}px)`;
                if (self.vars.touchzoneState.offsetModified) {
                    elements.touchzone.style.left = "";
                    self.vars.touchzoneState.offsetModified = false;
                }
                self.vars.touchzoneState.lengthModified = true;
            } else {
                if (self.vars.touchzoneState.lengthModified) {
                    elements.touchzone.style.width = "";
                    self.vars.touchzoneState.lengthModified = false;
                }
                if (self.vars.touchzoneState.offsetModified) {
                    elements.touchzone.style.left = "";
                    self.vars.touchzoneState.offsetModified = false;
                }
            }
        });
    }

    /**
     * getTranslation - calculate handle translation for given value
     *
     * @param {object} self control instance
     * @param {number} value widget value
     * @returns {number} handle translation
     */
    function getTranslation(self, value) {
        const { min, max } = self.vars.valueSettings,
            { dims, orientation } = self.vars;

        let tx = null;

        if (max === min) {
            tx = Math.max(0, dims.track - dims.handle);
        } else if (self.config.inverted) {
            tx = (1 - ((value - min) / (max - min))) *
                Math.max(0, dims.track - dims.handle);
        } else {
            tx = ((value - min) / (max - min)) *
                Math.max(0, dims.track - dims.handle);
        }

        if (orientation === orientations.VERTICAL) {
            tx *= -1;
        }

        return tx;
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            elements: {
                label: null,
                icon: null,
                value: null,
                unit: null,
                track: null,
                scale: null,
                scalefree: null,
                handle: null,
                touchzone: null
            },
            resizeObserver: null,
            listeners: [],
            tokens: [],
            valueSettings: null,
            value: 0,
            orientation: orientations.HORIZONTAL,
            dims: {
                track: null,
                handle: null,
                touchzone: null
            },
            dragging: false,
            handleTranslation: {
                tx: 0,
                ty: 0
            },
            anim: null,
            sendInterval: 0,
            touchzoneState: {
                lengthModified: false,
                offsetModified: false
            },
            touchzoneRafId: 0,
            firstDraw: true
        },
        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            iter: "visuals.tools.iterate.iterateObject",
            nv: "visuals.tools.numericValues",
            io: "visuals.io",
            gfx: "visuals.gfx"
        },
        /* array of custom event types fired by this control */
        events: [ "change" ],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { elements, listeners } = self.vars,
                    { iter, nv, io, gfx } = self.imports;

                iter(elements, (value, name) => {
                    elements[name] = shmi.getUiElement(name, self.element);
                });

                if (!elements.track || !elements.scale || !elements.handle) {
                    console.error(`[${className}] template is missing required element ("track", "scale", "handle")`);
                    return;
                }

                nv.initValueSettings(self);

                if (elements.value && typeof self.config["initial-text"] === "string") {
                    elements.value.textContent = shmi.localize(self.config["initial-text"]);
                }

                if (elements.value) {
                    setValuePopup(self, self.config["show-value-popup"]);
                }

                if (elements.label) {
                    setLabel(self, self.config["show-text"] ? self.config.label : null);
                }

                if (elements.unit) {
                    setUnit(self, self.config["unit-text"]);
                }

                if (elements.track.dataset.orientation === orientations.HORIZONTAL || elements.track.dataset.orientation === orientations.VERTICAL) {
                    self.vars.orientation = elements.track.dataset.orientation;
                } else {
                    console.warn(`[${className}] invalid orientation setting '${elements.track.dataset.orientation}' in template '${self.getConfig()['template']}', defaulting to horizontal.`);
                }

                if (self.vars.orientation === orientations.VERTICAL) {
                    elements.scale.style.transformOrigin = "bottom";
                } else {
                    elements.scale.style.transformOrigin = "left";
                }

                if (self.config.inverted) {
                    shmi.addClass(self.element, cssClasses.INVERTED);
                }

                initValuePopup(self);
                initIcon(self);

                self.vars.anim = new gfx.Animation(updateHandle.bind(null, self));

                self.vars.resizeObserver = new ResizeObserver(updateDimensions.bind(null, self));
                listeners.push(new io.MouseListener(elements.handle, getInputHandler(self), { dragThreshold: 0 }));
                listeners.push(new io.TouchListener(elements.handle, getInputHandler(self), true, { dragThreshold: 0 }));
                listeners.push(new io.MouseListener(elements.track, getSliderTrackInputHandler(self), { dragThreshold: 0 }));
                listeners.push(new io.TouchListener(elements.track, getSliderTrackInputHandler(self), true, { dragThreshold: 0 }));
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this,
                    { tokens, listeners } = self.vars,
                    { im } = self.imports;

                listeners.forEach((l) => l.enable());
                updateDimensions(self);
                self.vars.resizeObserver.observe(self.element);
                self.vars.firstDraw = true;
                if (self.config.item) {
                    tokens.push(im.subscribeItem(self.config.item, self));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.resizeObserver.unobserve(self.element);
                self.vars.listeners.forEach((l) => l.disable());
                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
                self.vars.anim.stop();
            },
            onDelete: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => {
                    if (!l.keepUnlocked) {
                        l.disable();
                    }
                });
                shmi.addClass(self.element, cssClasses.LOCKED);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => {
                    if (!l.keepUnlocked) {
                        l.enable();
                    }
                });
                shmi.removeClass(self.element, cssClasses.LOCKED);
            },
            /**
             * Sets properties for subscribed item
             */
            onSetProperties: function() {
                const self = this,
                    { nv } = self.imports;

                nv.setProperties(self, arguments);
            },
            /**
             * Set widget value
             *
             * @param {number} value value to set
             */
            onSetValue: function(value) {
                const self = this,
                    nv = shmi.requires("visuals.tools.numericValues"),
                    { elements, orientation, anim, handleTranslation } = self.vars,
                    config = self.getConfig(),
                    oldValue = self.getValue();

                self.vars.value = nv.applyInputSettings(value, self);

                if (elements.value) {
                    elements.value.textContent = nv.formatOutput(self.vars.value, self);
                }

                if (!self.vars.dragging) {
                    anim.setCurrentValue(orientation === orientations.VERTICAL ? handleTranslation.ty : handleTranslation.tx);
                    if (self.vars.firstDraw) {
                        self.vars.firstDraw = false;
                        anim.start(getTranslation(self, self.vars.value), 0);
                    } else {
                        anim.start(getTranslation(self, self.vars.value), config["animation-duration"]);
                    }
                }

                if (self.vars.value !== oldValue) {
                    self.fire("change", { value: self.vars.value });
                }
            },
            /**
             * Get widget value
             *
             * @returns {number} widget value
             */
            getValue: function() {
                return this.vars.value;
            },
            /**
             * Set widget label (when "show-text" & "auto-label" are enabled)
             *
             * @param {string} labelText label text
             */
            setLabel: function(labelText) {
                const self = this,
                    { elements } = self.vars;

                if (self.config["show-text"] && self.config['auto-label'] && elements.label) {
                    setLabel(self, labelText);
                    updateDimensions(self);
                }
            },
            /**
             * Set unit text (when "auto-unit-text" is enabled)
             *
             * @param {string} unitText unit text
             */
            setUnitText: function(unitText) {
                const self = this,
                    { elements } = self.vars;

                if (self.config['auto-unit-text'] && elements.unit) {
                    setUnit(self, unitText);
                    updateDimensions(self);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-text-display
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-text-display",
 *     "name": null,
 *     "template": "custom/controls/iq-text-display"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-unit-text": Whether to use the auto-unit (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-text-display", // control name in camel-case
        uiType = "iq-text-display", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-text-display iq-variant-01",
        "name": null,
        "template": "default/iq-text-display.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "precision": -1,
        "decimal-delimiter": "${DECIMAL_DELIMITER}",
        "type": shmi.c("TYPE_INT"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "auto-min": true,
        "auto-max": true,
        "auto-precision": true,
        "auto-type": true,
        "icon-src": null,
        "icon-class": null,
        "inline-svg-icon": true,
        "icon-title": null,
        "tooltip": null,
        "initial-text": "-",
        "show-icon": false,
        "show-text": true
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * setLabel - set label text for widget. applies "no-label" CSS class when no text is specified
     *
     * @param {object} self control instance
     * @param {string} [value] text value
     */
    function setLabel(self, value) {
        if (typeof value === "string" && value.length) {
            self.vars.labelEl.textContent = shmi.localize(value);
            shmi.removeClass(self.element, "no-label");
        } else {
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        tooltipProperties: ['icon-title'],

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            currentValueEl: null,
            labelEl: null,
            unitEl: null,
            iconEl: null,

            initialized: false,
            subscriptionTargetId: null,

            requestId: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { applyIcon } = shmi.requires("visuals.tools.icons");

                self.imports.nv.initValueSettings(self);

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.unitEl = shmi.getUiElement('unit', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.currentValueEl = shmi.getUiElement('value', self.element);

                // Check for required elements
                if (!self.vars.currentValueEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-text-display] Current value element is missing)", 3);
                    self.vars.labelEl = null;
                    self.vars.unitEl = null;
                    return;
                }

                // Unit
                if (self.vars.unitEl) {
                    if (self.config['unit-text']) { //} && !self.config['auto-unit-text']) {
                        self.vars.unit = self.config['unit-text'];
                        self.vars.unitEl.textContent = shmi.localize(self.config['unit-text']);
                    } else {
                        self.vars.unitEl.textContent = '';
                    }
                }

                if (this.config['unit-scale']) {
                    this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
                }

                // Label
                if (self.vars.labelEl && self.config["show-text"]) {
                    setLabel(self, self.config.label);
                } else {
                    setLabel(self, null);
                }

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-button] no button-icon element provided', 1);
                    shmi.addClass(self.element, "no-icon");
                } else if (self.config["show-icon"] && self.config['icon-src']) {
                    applyIcon(self.vars.iconEl, self.config["icon-src"], null, self.config["inline-svg-icon"]);
                } else if (self.config["show-icon"] && self.config['icon-class']) {
                    applyIcon(self.vars.iconEl, null, self.config["icon-class"]);
                } else {
                    shmi.addClass(self.element, "no-icon");
                }

                if (typeof self.config["initial-text"] === "string") {
                    self.vars.currentValueEl.textContent = shmi.localize(self.config["initial-text"]);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-text-display] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-text-display] disabled", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;

                if (type === shmi.c("TYPE_STRING")) {
                    self.vars.value = value;
                } else if (isNaN(parseFloat(value))) {
                    self.vars.value = value;
                } else {
                    self.vars.value = parseFloat(value);
                }

                shmi.caf(self.vars.requestId);
                self.vars.requestId = shmi.raf(function() {
                    self.asyncSet(type);
                });
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function() {
                const self = this,
                    { nv } = self.imports;

                nv.setProperties(self, arguments);
            },

            setUnitText: function(unitText) {
                var self = this;
                if (self.vars.unitEl && self.config['auto-unit-text']) {
                    self.vars.unit = unitText;
                    self.vars.unitEl.textContent = shmi.localize(unitText);
                }
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabel(self, labelText);
                }
            },

            /**
             * used to set the current value asynchronously
             */
            asyncSet: function(type) {
                var self = this;

                if (shmi.visuals.session.deactivateControls) {
                    setTimeout(self.asyncSet.bind(self, type), shmi.c("ACTION_RETRY_TIMEOUT"));
                } else {
                    self.vars.currentValueEl.textContent = self.imports.nv.formatOutput(self.vars.value, self);
                }
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-text
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-text",
 *     "name": null,
 *     "template": "custom/controls/iq-text"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "items": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-text", // control name in camel-case
        uiType = "iq-text", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-text iq-variant-01",
        "name": null,
        "template": "default/iq-text.iq-variant-01",
        "text": '[Text]',
        "value-not-available": "${label.no-value}",
        "items": [],
        "tooltip": null
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            tokens: [],
            textEl: null,
            itemValues: [],
            rafId: 0
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.textEl = shmi.getUiElement('text', self.element);

                if (!self.vars.textEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-text] Missing main element", 3);
                    return;
                }

                if (Array.isArray(self.config.items)) {
                    self.config.items.forEach((item) => {
                        self.vars.itemValues.push({
                            value: (typeof self.config['value-not-available'] === "string") ? shmi.localize(self.config['value-not-available']) : null,
                            unit: null,
                            label: null
                        });
                    });
                }

                if (self.config.text) {
                    self.vars.textEl.textContent = shmi.evalString(shmi.localize(self.config.text), {
                        items: self.vars.itemValues
                    });
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                var self = this;

                if (Array.isArray(self.config.items)) {
                    self.config.items.forEach(function(item, idx) {
                        var handler = self.imports.im.getItemHandler();

                        handler.setValue = function setValue(value, type, name) {
                            const handlerItem = self.imports.im.getItem(name);
                            let unit = null;

                            if (handlerItem) {
                                if (handlerItem.adapter && handlerItem.adapter.unitText) {
                                    unit = handlerItem.adapter.unitText;
                                } else if (typeof handlerItem.unit === "string") {
                                    unit = handlerItem.unit;
                                }
                                self.vars.itemValues[idx] = {
                                    value: (type === shmi.Constants.TYPE_STRING) ? handlerItem.readValue() : self.imports.nv.formatNumber(handlerItem.readValue(true), {
                                        "precision": handlerItem.digits,
                                        "unit": handlerItem.unit,
                                        "show-unit": false
                                    }),
                                    unit: shmi.localize(unit),
                                    label: shmi.localize(handlerItem.labelToken)
                                };
                                self.updateText();
                            }
                        };
                        self.vars.tokens.push(self.imports.im.subscribeItem(item, handler));
                    });
                }

                shmi.log("[IQ:iq-text] Enabled", 1);
            },

            /* Called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                shmi.log("[IQ:iq-text] disabled", 1);
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                shmi.log("[IQ:iq-text] Locked", 1);
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                shmi.removeClass(self.element, 'locked');
                shmi.log("[IQ:iq-text] unlocked", 1);
            },

            /** Updates the text **/
            updateText: function() {
                var self = this;

                shmi.caf(self.vars.rafId);
                self.vars.rafId = shmi.raf(function() {
                    if (!self.isActive()) {
                        return;
                    }
                    self.vars.textEl.textContent = shmi.evalString(shmi.localize(self.config.text), {
                        items: self.vars.itemValues
                    });
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-trend-legend
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-legend",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-legend"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-legend", // control name in camel-case
        uiType = "iq-trend-legend", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-legend",
        "name": null,
        "template": "default/iq-trend-legend",
        "trend-widget": null,
        "show-unit": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * update legend display when graph settings change
     *
     * @param {object} self control instance
     */
    function handleGraphChange(self) {
        clearValueInfo(self);
        generateValueInfo(self);
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        self.vars.styleImages = loadStyleImages(self, control.getValueStyles());
        self.vars.defaultStyleImage = loadStyleImages(self, [control.getDefaultStyle()])[0];
        self.vars.inactiveImage = loadStyleImages(self, [control.getInactiveStyle()])[0];
        generateValueInfo(self);
        self.vars.infoToken = control.listen("value-info", handleGraphChange.bind(null, self));
        self.vars.optionsToken = control.listen("graph-options", handleGraphChange.bind(null, self));
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        clearValueInfo(self);
        self.vars.trendWidget = null;
        if (self.vars.infoToken) {
            self.vars.infoToken.unlisten();
            self.vars.infoToken = null;
        }

        if (self.vars.optionsToken) {
            self.vars.optionsToken.unlisten();
            self.vars.optionsToken = null;
        }
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    /**
     * clear generated value info elements
     *
     * @param {object} self control instance
     */
    function clearValueInfo(self) {
        self.vars.listeners.forEach((l) => {
            l.disable();
        });
        self.vars.listeners = [];
        self.vars.valueInfos.forEach((vi) => {
            vi.base.parentNode.removeChild(vi.base);
        });
        self.vars.valueInfos = [];
    }

    /**
     * check if unit is a unit-class
     *
     * @param {string|number} unitText configured unit
     * @return {boolean} `true` if unit is unit-class, `false` else
     */
    function isUnitClass(unitText) {
        return String(parseInt(unitText)) === String(unitText);
    }

    /**
     * generate value info
     *
     * @param {object} self control instance
     */
    function generateValueInfo(self) {
        const { trendWidget } = self.vars;
        if (!trendWidget) {
            return;
        }

        const valueInfo = self.vars.trendWidget.getValueInfo(),
            selectedInfo = self.vars.trendWidget.getSelectedValues(),
            uc = shmi.requires("visuals.tools.unitClasses"),
            highlightActive = selectedInfo.some((vi) => !!(vi.options && vi.options.highlight));

        if (!highlightActive) {
            if (self.vars.highlighted) {
                shmi.removeClass(self.vars.highlighted.base, "active");
            }
            self.vars.highlighted = null;
            shmi.removeClass(self.element, "highlighted");
        } else {
            shmi.addClass(self.element, "highlighted");
        }

        self.vars.valueInfos = selectedInfo.filter((info) => valueInfo[info.name] && typeof valueInfo[info.name].data_type === "string" && valueInfo[info.name].data_type !== "string").map((info) => {
            const template = self.vars.elements.valueInfoTemplate.content.cloneNode(true),
                valueElement = shmi.getUiElement("value-info", template),
                base = valueElement ? valueElement : null,
                label = base ? shmi.getUiElement("value-label", base) : null,
                icon = base ? shmi.getUiElement("value-icon", base) : null,
                unit = base ? shmi.getUiElement("value-unit", base) : null;

            if (label) {
                label.textContent = valueInfo[info.name] && valueInfo[info.name].label ? shmi.localize(valueInfo[info.name].label) : info.name;
            }

            if (icon) {
                if (highlightActive && !info.options.highlight) {
                    icon.setAttribute("src", self.vars.inactiveImage);
                } else {
                    const styleImage = self.vars.styleImages[info.options.style] ? self.vars.styleImages[info.options.style] : self.vars.defaultStyleImage;
                    icon.setAttribute("src", styleImage);
                }
            }

            let unitText = "";
            if (base && unit) {
                if (self.config["show-unit"] && valueInfo[info.name] && valueInfo[info.name].unit !== null) {
                    if (isUnitClass(valueInfo[info.name].unit)) {
                        const ucAdapter = uc.getSelectedAdapter(valueInfo[info.name].unit);
                        if (ucAdapter) {
                            unitText = ucAdapter.unitText;
                        }
                    } else {
                        unitText = valueInfo[info.name].unit;
                    }
                }

                if (typeof unitText === "string" && unitText !== "") {
                    unit.textContent = shmi.localize(unitText);
                } else {
                    shmi.addClass(base, "no-unit");
                }
            }

            const infoRef = {
                base,
                label,
                icon,
                unit,
                info
            };

            if (base) {
                if ((info.options && info.options.highlight) || (self.vars.highlighted && self.vars.highlighted.info.name === info.name)) {
                    if (self.vars.highlighted && self.vars.highlighted !== infoRef) {
                        shmi.removeClass(self.vars.highlighted.base, "active");
                    }
                    self.vars.highlighted = infoRef;
                    shmi.addClass(base, "active");
                }
                const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                    handler = {
                        onPress: () => { /* implementation of onPress handler stops event bubbling */ },
                        onClick: () => {
                            const highlighted = self.vars.highlighted;
                            trendWidget.setGraphOptions(info.name, { highlight: (highlighted === null) || (highlighted !== infoRef) });
                        }
                    };

                self.vars.listeners.push(new MouseListener(base, handler), new TouchListener(base, handler));
            }

            return infoRef;
        });

        self.vars.valueInfos.forEach((vi) => {
            if (vi.base) {
                self.vars.elements.valueAnchor.appendChild(vi.base);
            }
        });

        if (!self.isLocked()) {
            self.vars.listeners.forEach((l) => {
                l.enable();
            });
        }
    }

    /**
     * load images to use for style preview
     *
     * @param {object} self control instance
     * @param {object[]} styles curve style configuration
     * @return {string[]} image urls for style preview
     */
    function loadStyleImages(self, styles) {
        if (!self.vars.elements.styleIconTemplate) {
            return [];
        }
        const dataPrefix = "data:image/svg+xml;base64,",
            options = styles.map((style) => {
                const template = self.vars.elements.styleIconTemplate.content.cloneNode(true),
                    svg = template.querySelector("svg");
                if (svg) {
                    const line = svg.querySelector(".iqel-curve-line");
                    if (line) {
                    //set style...
                        line.style.setProperty("fill", style.value.strokeStyle);
                    }
                    const fill = svg.querySelector(".iqel-curve-fill");
                    if (fill) {
                    //set style...
                        fill.style.setProperty("fill", style.value.fillStyle);
                    }
                    return `${dataPrefix}${btoa(svg.outerHTML)}`;
                }

                return null;
            }).filter((imgData) => imgData !== null);

        return options;
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                valueAnchor: null,
                valueInfoTemplate: null,
                styleIconTemplate: null
            },
            styleImages: [],
            defaultStyleImage: null,
            inactiveImage: null,
            valueInfos: [],
            infoToken: null,
            optionsToken: null,
            tokens: [],
            listeners: [],
            highlighted: null
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.elements.valueAnchor = shmi.getUiElement("value-anchor", self.element);
                self.vars.elements.valueInfoTemplate = shmi.getUiElement("value-info-template", self.element);
                self.vars.elements.styleIconTemplate = shmi.getUiElement("style-icon-template", self.element);

                if (self.vars.elements.valueAnchor && self.config.orientation === "vertical") {
                    shmi.addClass(self.vars.elements.valueAnchor, "vertical");
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                const { elements } = this.vars;
                if (!(elements.valueAnchor && elements.valueInfoTemplate && elements.styleIconTemplate)) {
                    console.error(`[${uiType}] template is missing required elements.`);
                    return;
                }
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
            },

            /* Called when control is deleted */
            onDelete: function() {

            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend-live
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-live",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-live"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-live", // control name in camel-case
        uiType = "iq-trend-live", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-live",
        "name": null,
        "template": "default/iq-trend-live",
        "trend-widget": null,
        "orientation": "horizontal"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * update active state of button elements
     *
     * @param {object} self control instance
     * @param {boolean} isLive state of live mode
     */
    function updateButtonState(self, isLive) {
        if (isLive) {
            shmi.removeClass(self.vars.elements.pauseButton, "active");
            shmi.addClass(self.vars.elements.playButton, "active");
        } else {
            shmi.removeClass(self.vars.elements.playButton, "active");
            shmi.addClass(self.vars.elements.pauseButton, "active");
        }
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        if (!self.isLocked()) {
            shmi.removeClass(self.element, "locked");
        }
        updateButtonState(self, control.isLive());
        self.vars.tokens.push(control.listen("live", (evt) => {
            updateButtonState(self, evt.detail.isLive);
        }));
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        if (self.isActive() && !self.isLocked()) {
            shmi.addClass(self.element, "locked");
        }
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                playButton: null,
                pauseButton: null
            },
            tokens: [],
            listeners: []
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { MouseListener, TouchListener } = shmi.requires("visuals.io");

                self.vars.elements.playButton = shmi.getUiElement("play-button", self.element);
                self.vars.elements.pauseButton = shmi.getUiElement("pause-button", self.element);

                if (self.config.orientation === "vertical") {
                    shmi.addClass(self.element, "vertical");
                }

                if (self.vars.elements.playButton && self.vars.elements.pauseButton) {
                    const playHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget && !self.vars.trendWidget.isLive()) {
                                self.vars.trendWidget.setLive(true);
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.playButton, playHandler),
                        new TouchListener(self.vars.elements.playButton, playHandler)
                    );

                    const pauseHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget && self.vars.trendWidget.isLive()) {
                                self.vars.trendWidget.setLive(false);
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.pauseButton, pauseHandler),
                        new TouchListener(self.vars.elements.pauseButton, pauseHandler)
                    );
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                const { elements } = this.vars;
                if (!(elements.playButton && elements.pauseButton)) {
                    console.error(`[${uiType}] template is missing required elements.`);
                    return;
                }
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            },

            /* Called when control is disabled */
            onDisable: function() {
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is deleted */
            onDelete: function() {

            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend-select
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-select",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-select"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "items": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-select", // control name in camel-case
        uiType = "iq-trend-select", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-select",
        "name": null,
        "template": "default/iq-trend-select",
        "trend-widget": null,
        "label-mode": "label",
        "settingsDialogOptions": {}
    };

    /**
     * get configuration for complex-table2 instance
     *
     * @param {string} gridName datagrid name
     * @param {string} [labelMode="label"] label-mode to use, either `"label"` or `"item"`
     * @param {string} widgetHandle iq-trend-select instance node handle
     */
    const getTableConfig = (gridName, labelMode = "label", widgetHandle) => ({
        "label": "Trend Select Table",
        "table": gridName,
        "name": "trend-select-table",
        "class-name": "complex-table2 trend-item-selection",
        "field-datagrid-col-map": {
            "selected": 3,
            "name": 0,
            "label": 1,
            "options": 2,
            "image-url": 4
        },
        "default-field-control-map": {
            "selected": {
                "ui-type": "iq-checkbox",
                "config": {
                    "class-name": "iq-checkbox iq-variable-01",
                    "template": "default/iq-checkbox.iq-variant-01",
                    "label": ""
                }
            },
            "name": {
                "ui-type": "iq-label",
                "config": {
                    "class-name": "iq-label iq-variant-01"
                }
            },
            "label": {
                "ui-type": "iq-label",
                "config": {
                    "class-name": "iq-label variant-01"
                }
            },
            "options": {
                "ui-type": "local-script",
                "config": {
                    "module": "visuals.controls.iqtrendselect.ls.settings",
                    "handle": widgetHandle
                }
            },
            "image-url": {
                "ui-type": "local-script",
                "config": {
                    "module": "visuals.controls.iqtrendselect.ls.style",
                    "handle": widgetHandle
                }
            }
        },
        "default-field-headers": {
            "selected": " ",
            "name": "${iqtrendselect.table.value.header}",
            "label": "${iqtrendselect.table.value.header}",
            "options": " ",
            "image-url": " "
        },
        "_comment": "expr is passed to expr parameter array of shmi.visuals.core.DataGridManager.setFilter",
        "filters": [],
        "default-layout": {
            "class-name": "layout-std",
            "_comment": "default == no additional css layout class",
            "column-org": {
                "col1": {
                    "fields": ["selected"],
                    "column-width": "var(--column-checkbox-width)"
                },
                "col2": {
                    "fields": [ labelMode === "label" ? "label" : "name"],
                    "column-width": "var(--column-name-width)"
                },
                "col3": {
                    "fields": ["image-url"],
                    "column-width": "var(--column-style-width)"
                },
                "col4": {
                    "fields": ["options"],
                    "column-width": "var(--column-edit-width)"
                }
            },
            "line-height": "var(--table-row-height)"
        },
        "sortable-fields": [
            "selected",
            "name",
            "label"
        ],
        "delete-selected-rows": false,
        "show-nof-rows": true,
        "show-buttons-table-min-width-px": 400,
        "text-mode": "SINGLELINE",
        "responsive-layouts": [],
        "default-nof-buffered-rows": 60,
        "buffer-size": 500,
        "quicksearch": {
            "enable": true,
            "remember": true,
            "fields": [
                "name",
                "label"
            ]
        },
        "select-mode": "SINGLE",
        "show-select-boxes": false
    });

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * get random id for datagrid naming
     *
     * @return {string}
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        self.vars.styleImages = loadStyleImages(self, control.getValueStyles());
        self.vars.infoToken = control.listen("value-info", (evt) => {
            self.vars.grid.refresh();
        });
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        self.vars.infoToken.unlisten();
        self.vars.infoToken = null;
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    /**
     * load images to use for style preview
     *
     * @param {object} self control instance
     * @param {object[]} styles curve style configuration
     * @return {string[]} image urls for style preview
     */
    function loadStyleImages(self, styles) {
        if (!self.vars.elements.styleIconTemplate) {
            return [];
        }
        const dataPrefix = "data:image/svg+xml;base64,",
            options = styles.map((style) => {
                const div = document.createElement("div");
                div.innerHTML = self.vars.elements.styleIconTemplate.innerHTML;
                const svg = div.querySelector("svg");
                if (svg) {
                    const line = svg.querySelector(".curve-line");
                    if (line) {
                    //set style...
                        line.style.setProperty("fill", style.value.strokeStyle);
                    }
                    const fill = svg.querySelector(".curve-fill");
                    if (fill) {
                    //set style...
                        fill.style.setProperty("fill", style.value.fillStyle);
                    }
                    return `${dataPrefix}${btoa(div.innerHTML)}`;
                }

                return null;
            }).filter((imgData) => imgData !== null);

        return options;
    }

    /**
     * store value settings of specified value name
     *
     * @param {object} self control instance
     * @param {string} valueName name of value
     */
    function storeValueSettings(self, valueName) {
        if (self.vars.trendWidget) {
            const selectedValues = self.vars.trendWidget.getSelectedValues(),
                valueSettings = selectedValues.find(({ name }) => name === valueName);

            if (valueSettings) {
                self.vars.storedSettings[valueName] = valueSettings;
            }
        }
    }

    /**
     * get value settings for newly selected value
     *
     * @param {object} self control instance
     * @param {string} valueName name of value
     * @param {object[]} rowData datagrid row data
     * @return {object} value settings
     */
    function getValueSettings(self, valueName, rowData) {
        const storedSettings = self.vars.storedSettings[valueName] ?? null,
            selectedValues = self.vars.trendWidget.getSelectedValues(),
            valueStyles = self.vars.trendWidget.getValueStyles(),
            usedStyles = selectedValues.map(
                (value) => (value.options && typeof value.options.style === "number" ? value.options.style : null)
            ).filter((idx) => idx !== null),
            storedStyles = Object.values(self.vars.storedSettings).map((value) => value.options.style),
            preferredStyle = valueStyles.findIndex((style, idx) => !usedStyles.includes(idx) && !storedStyles.includes(idx)),
            newStyle = preferredStyle > -1 ? preferredStyle : valueStyles.findIndex((style, idx) => !usedStyles.includes(idx)),
            options = JSON.parse(rowData[2].value);

        const valueSettings = storedSettings ? storedSettings.options : {
            fillCurve: false,
            drawTolerance: false,
            style: typeof newStyle === "number" ? newStyle : undefined,
            scale: {
                location: "left",
                hidden: options.item.data_type === "string"
            },
            limits: {
                mode: "config"
            },
            scaleGroup: null
        };

        return valueSettings;
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                tableAnchor: null,
                styleIconTemplate: null
            },
            styleImages: [],
            defaultStyleImage: null,
            grid: null,
            infoToken: null,
            tokens: [],
            storedSettings: {}
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    gridUtil = shmi.requires("visuals.controls.iqtrendselect.grid"),
                    dgm = shmi.requires("visuals.session.DataGridManager"),
                    tm = shmi.requires("visuals.core.TrendManager2");
                let gridId = null;
                do {
                    gridId = className + "-" + getRandomId();
                } while (dgm.getGrid(gridId) !== null);

                self.vars.elements.styleIconTemplate = shmi.getUiElement("style-icon-template", self.element);

                //create datagrid for item selection
                self.vars.grid = gridUtil.create({
                    name: gridId,
                    fields: ["name", "label", "options", "selected", "image-url"],
                    index: ["name"],
                    mode: gridUtil.FILTER_MODE_OR
                }, (callback) => {
                    if (self.vars.trendWidget) {
                        const trendInfo = self.vars.trendWidget.getTrendInfo(),
                            valueInfo = self.vars.trendWidget.getValueInfo(),
                            styles = self.vars.trendWidget.getValueStyles(),
                            selectedInfo = self.vars.trendWidget.getSelectedValues(),
                            scaleGroups = [];

                        if (!trendInfo) {
                            callback([]);
                        }
                        self.vars.styleImages = loadStyleImages(self, styles);
                        self.vars.defaultStyleImage = loadStyleImages(self, [self.vars.trendWidget.getDefaultStyle()]);

                        selectedInfo.forEach((variable) => {
                            if (variable.options && typeof variable.options.scaleGroup === "number" && !scaleGroups.includes(variable.options.scaleGroup)) {
                                scaleGroups.push(variable.options.scaleGroup);
                            }
                        });

                        tm.getInfo(trendInfo.name).then((response) => {
                            if (response && Array.isArray(response.items)) {
                                const rows = response.items.map((item, idx) => {
                                    const selected = selectedInfo.find((variable) => variable.name === item.name);

                                    return [

                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: item.name,
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: item.label ? shmi.localize(item.label) : item.name,
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: JSON.stringify({
                                                data: selected ? selected : null,
                                                scaleGroups: scaleGroups,
                                                styles: styles,
                                                item
                                            }),
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_BOOL"),
                                            value: valueInfo && valueInfo[item.name] ? 1 : 0,
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: selected && selected.options && typeof selected.options.style === "number" ? self.vars.styleImages[selected.options.style] : "",
                                            min: null,
                                            max: null
                                        }
                                    ];
                                });
                                callback(rows);
                            } else {
                                callback([]);
                            }
                        }).catch((exc) => {
                            console.error("Error retrieving trend info:", exc);
                            callback([]);
                        });
                    } else {
                        callback([]);
                    }
                });
                self.vars.elements.tableAnchor = shmi.getUiElement("table-anchor", self.element);
                if (self.vars.elements.tableAnchor) {
                    shmi.createControl("complex-table2", self.vars.elements.tableAnchor, getTableConfig(gridId, self.config["label-mode"], shmi.requires("visuals.tools.nodes").getNodeHandle(self)), "DIV");
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }

                this.controls.forEach((c) => {
                    c.enable();
                });

                //listen for selection change on datagrid
                this.vars.tokens.push(shmi.listen("grid-changed", (evt) => {
                    if (!this.vars.trendWidget) {
                        return;
                    }
                    const rowData = evt.source.getRowData(evt.detail.row);

                    if (rowData) {
                        if (evt.detail.value === 1) {
                            const valueSettings = getValueSettings(this, rowData[0].value, rowData);

                            this.vars.trendWidget.addGraphValue(rowData[0].value, valueSettings);
                            delete this.vars.storedSettings[rowData[0].value];
                        } else {
                            storeValueSettings(this, rowData[0].value);
                            this.vars.trendWidget.removeGraph(rowData[0].value);
                        }
                    }
                }, { "detail.grid.name": this.vars.grid.name, "detail.field": "selected" }));

                //listen for changes to display options on datagrid
                this.vars.tokens.push(shmi.listen("grid-changed", (evt) => {
                    if (this.vars.trendWidget) {
                        const config = JSON.parse(evt.detail.value);
                        this.vars.trendWidget.setGraphOptions(config.data.name, config.data.options);
                        this.vars.grid.refresh();
                    }
                }, { "detail.grid.name": this.vars.grid.name, "detail.field": "options" }));
            },
            /**
             * get images to preview curve styles
             *
             * @returns {string[]} curve style images
             */
            getStyleImages() {
                return shmi.cloneObject(this.vars.styleImages);
            },
            /**
             * get image to preview default curve style
             *
             * @return {string} curve style image
             */
            getDefaultStyleImage() {
                return this.vars.defaultStyleImage;
            },
            /* Called when control is disabled */
            onDisable: function() {
                this.controls.forEach((c) => {
                    c.disable();
                });
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
            },

            /* Called when control is deleted */
            onDelete: function() {
                const gridUtil = shmi.requires("visuals.controls.iqtrendselect.grid");
                if (this.vars.grid) {
                    gridUtil.remove(this.vars.grid.name);
                    this.vars.grid = null;
                }
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * Module to implement dialog used for trend curve settings
 *
 * @module visuals/controls/iqtrendselect/dialog
 */
(function() {
    const MODULE_NAME = "visuals.controls.iqtrendselect.dialog",
        /** @lends module:visuals/controls/iqtrendselect/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "trend-select-config-dialog";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * initialize style selection options
     *
     * @param {object} state controller state
     * @param {string[]} styleImages style preview image urls
     */
    function initStyles(state, styleImages) {
        const selectStyle = state.getInstance("selectStyle");

        selectStyle.setOptions(styleImages.map((imgData, idx) => ({
            "label": `\${iqtrendselect.settings.style.option} ${idx + 1}`,
            "value": idx,
            "icon-src": imgData
        })));
    }

    /**
     * check if required widgets are ready
     *
     * @param {object} state controller state
     * @return {boolean} `true` if widgets are ready, `false` else
     */
    function widgetsReady(state) {
        return [
            "selectStyle",
            "checkTolerance",
            "checkFill",
            "selectLimitsMode",
            "inputLimitsStep",
            "inputLimitsMin",
            "inputLimitsMax",
            "selectScaleLocation",
            "checkScaleHide",
            "selectScaleGroup",
            "buttonScaleAddGroup",
            "buttonApply",
            "buttonCancel"
        ].every((slotName) => !!state.getInstance(slotName));
    }

    /**
     * set controller slot values
     *
     * @param {object} state controller state
     * @param {object} slotData slot data
     */
    function setSlotValues(state, slotData) {
        const { cellInfo } = state.data,
            scaleGroupOptions = [
                {
                    label: "${iqtrendselect.settings.scalegroup.none}",
                    value: null
                }
            ];

        state.data.ignoreEvents = true;

        cellInfo.scaleGroups.forEach((groupId) => {
            scaleGroupOptions.push({
                label: `${groupId}`,
                value: groupId
            });
        });
        state.getInstance("selectScaleGroup").setOptions(scaleGroupOptions);

        state.getInstance("selectStyle").setValue(slotData.style);
        state.getInstance("checkTolerance").setValue(!!slotData.drawTolerance);
        state.getInstance("checkFill").setValue(!!slotData.fillCurve);
        state.getInstance("selectLimitsMode").setValue(slotData.limits && slotData.limits.mode ? slotData.limits.mode : "config");
        state.getInstance("inputLimitsStep").setValue(slotData.limits && typeof slotData.limits.step === "number" ? slotData.limits.step : 0);
        state.getInstance("inputLimitsMin").setValue(slotData.limits && typeof slotData.limits.min === "number" ? slotData.limits.min : 0);
        state.getInstance("inputLimitsMax").setValue(slotData.limits && typeof slotData.limits.max === "number" ? slotData.limits.max : 100);
        state.getInstance("selectScaleLocation").setValue(slotData.scale && typeof slotData.scale.location === "string" ? slotData.scale.location : "left");
        state.getInstance("checkScaleHide").setValue(slotData.scale && typeof slotData.scale.hidden === "boolean" ? slotData.scale.hidden : false);
        state.getInstance("selectScaleGroup").setValue(typeof slotData.scaleGroup === "number" ? slotData.scaleGroup : null);

        state.data.ignoreEvents = false;

        updateState(state);
    }

    /**
     * get controller slot values
     *
     * @param {object} state controller state
     * @return {object} slot data
     */
    function getSlotValues(state) {
        const options = {
            style: state.getInstance("selectStyle").getValue(),
            drawTolerance: state.getInstance("checkTolerance").getValue(),
            fillCurve: state.getInstance("checkFill").getValue(),
            limits: {
                mode: state.getInstance("selectLimitsMode").getValue(),
                step: state.getInstance("inputLimitsStep").getValue(),
                min: state.getInstance("inputLimitsMin").getValue(),
                max: state.getInstance("inputLimitsMax").getValue()
            },
            scale: {
                location: state.getInstance("selectScaleLocation").getValue(),
                hidden: state.getInstance("checkScaleHide").getValue()
            },
            scaleGroup: state.getInstance("selectScaleGroup").getValue()
        };

        if (options.limits.mode === "config") {
            delete options.limits.step;
            delete options.limits.min;
            delete options.limits.max;
        } else if (options.limits.mode === "auto") {
            if (options.limits.step === 0) {
                //disable stepping
                options.limits.step = null;
            }
            delete options.limits.min;
            delete options.limits.max;
        } else {
            delete options.limits.step;
            if (options.limits.min > options.limits.max) {
                //switch min & max
                const tmpMin = options.limits.min;
                options.limits.min = options.limits.max;
                options.limits.max = tmpMin;
            }
        }

        return options;
    }

    /**
     * handle click on add scalegroup button
     *
     * @param {object} state controller state
     */
    function handleAddScalegroup(state) {
        state.data.ignoreEvents = true;
        const selectGroup = state.getInstance("selectScaleGroup");
        const options = selectGroup.getOptions();
        const groups = options.map((option) => option.value).filter((value) => value !== null);
        let newGroup = 1;
        while (groups.includes(newGroup)) {
            newGroup += 1;
        }
        options.push({
            label: `${newGroup}`,
            value: newGroup
        });
        selectGroup.setOptions(options);
        selectGroup.setValue(newGroup);
        state.getInstance("checkScaleHide").setValue(false);
        state.data.ignoreEvents = false;
    }

    /**
     * handle controller slot events
     *
     * @param {object} state controller state
     * @param {string} slot slot name
     * @param {string} type event type
     * @param {object} event event reference
     */
    function handleSlotEvent(state, slot, type, event) {
        if (state.data.ignoreEvents) {
            return;
        }
        switch (slot) {
        case "dialog":
        case "buttonCancel":
            if (!state.data.resolved) {
                state.data.resolved = true;
                state.data.resolve(null);
            }
            shmi.deleteControl(state.getInstance("dialog"));
            break;
        case "selectLimitsMode":
            updateState(state);
            break;
        case "buttonApply":
            state.data.resolved = true;
            state.data.resolve(getSlotValues(state));
            state.getInstance("dialog").hide();
            break;
        case "buttonScaleAddGroup":
            handleAddScalegroup(state);
            break;
        default:
        }
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {object} options curve options
     * @param {object} options.cellInfo curve information of current table cell
     * @param {string[]} styleImages style preview image urls
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(options, styleImages, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                selectStyle: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                checkTolerance: {
                    ui: "iq-checkbox",
                    optional: true,
                    events: ["change"]
                },
                checkFill: {
                    ui: "iq-checkbox",
                    optional: true,
                    events: ["change"]
                },
                selectLimitsMode: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                inputLimitsStep: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                inputLimitsMin: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                inputLimitsMax: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                selectScaleLocation: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                checkScaleHide: {
                    ui: "iq-checkbox",
                    optional: true,
                    events: ["change"]
                },
                selectScaleGroup: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                buttonScaleAddGroup: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {
                if (widgetsReady(state) && !state.data.contentSet) {
                    initStyles(state, styleImages);
                    //all widgets ready -> set data & update state
                    setSlotValues(state, (options.cellInfo && options.cellInfo.data) ? options.cellInfo.data.options : {});
                    updateState(state);
                    state.data.contentSet = true;
                }
            },
            onEnable: function(state) {
                state.data.cellInfo = options.cellInfo;
                state.data.resolve = resolve;
            },
            onDisable: function(state) {

            },
            onEvent: handleSlotEvent,
            data: {
                cellInfo: null,
                ignoreEvents: false,
                resolve: null,
                resolved: false,
                contentSet: false
            }
        };
    }

    /**
     * update controller display state
     *
     * @param {object} state controller state
     */
    function updateState(state) {
        if (!widgetsReady(state)) {
            return;
        }
        const selectLimitsMode = state.getInstance("selectLimitsMode"),
            inputLimitsStep = state.getInstance("inputLimitsStep"),
            inputLimitsMin = state.getInstance("inputLimitsMin"),
            inputLimitsMax = state.getInstance("inputLimitsMax");

        const mode = selectLimitsMode.getValue();
        switch (mode) {
        case "config":
            shmi.addClass(inputLimitsStep.element, "hidden");
            shmi.addClass(inputLimitsMin.element, "hidden");
            shmi.addClass(inputLimitsMax.element, "hidden");
            break;
        case "auto":
            shmi.removeClass(inputLimitsStep.element, "hidden");
            shmi.addClass(inputLimitsMin.element, "hidden");
            shmi.addClass(inputLimitsMax.element, "hidden");
            break;
        case "manual":
            shmi.addClass(inputLimitsStep.element, "hidden");
            shmi.removeClass(inputLimitsMin.element, "hidden");
            shmi.removeClass(inputLimitsMax.element, "hidden");
            break;
        default:
        }
    }

    /**
     * get configured ui layout for curve configuration dialog
     *
     * @param {object} options dialog options
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.selectStyleClassName] class-name to apply to style select iq-selectbox
     * @param {string} [options.selectStyleTemplate] template to use for style select iq-selectbox
     * @param {string} [options.checkFillClassName] class-name to apply to fill curve iq-checkbox
     * @param {string} [options.checkFillTemplate] template to use for fill curve iq-checkbox
     * @param {string} [options.checkToleranceClassName] class-name to apply to draw tolerances iq-checkbox
     * @param {string} [options.checkToleranceTemplate] template to use for draw tolerances iq-checkbox
     * @param {string} [options.labelLimitsClassName] class-name to apply to limits section iq-label
     * @param {string} [options.labelLimitsTemplate] template to use for limits section iq-label
     * @param {string} [options.selectLimitsModeClassName] class-name to apply to limit mode iq-selectbox
     * @param {string} [options.selectLimitsModeTemplate] template to use for limit mode iq-selectbox
     * @param {string} [options.inputLimitsStepClassName] class-name to apply to limit step iq-input-field
     * @param {string} [options.inputLimitsStepTemplate] template to use for limit step iq-input-field
     * @param {string} [options.inputLimitsMinClassName] class-name to apply to limit min iq-input-field
     * @param {string} [options.inputLimitsMinTemplate] template to use for limit min iq-input-field
     * @param {string} [options.inputLimitsMaxClassName] class-name to apply to limit max iq-input-field
     * @param {string} [options.inputLimitsMaxTemplate] template to use for limit max iq-input-field
     * @param {string} [options.labelScaleClassName] class-name to apply to scale section iq-label
     * @param {string} [options.labelScaleTemplate] template to use for scale section iq-label
     * @param {string} [options.selectScaleLocationClassName] class-name to apply to scale location iq-selectbox
     * @param {string} [options.selectScaleLocationTemplate] template to use for scale location iq-selectbox
     * @param {string} [options.checkScaleHideClassName] class-name to apply to hide scale iq-checkbox
     * @param {string} [options.checkScaleHideTemplate] template to use for hide scale iq-checkbox
     * @param {string} [options.selectScaleGroupClassName] class-name to apply to scale group iq-selectbox
     * @param {string} [options.selectScaleGroupTemplate] template to use for scale group iq-selectbox
     * @param {string} [options.addGroupClassName] class-name to apply to add scale group iq-button
     * @param {string} [options.addGroupTemplate] template to use for add scale group iq-button
     * @param {string} [options.applyClassName] class-name to apply to apply settings iq-button
     * @param {string} [options.applyTemplate] template to use for apply settings iq-button
     * @param {string[]} styleImages style preview image urls
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(options, styleImages, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "curveSettingsDialog",
                "class-name": options.dialogClassName || "dialog-box iq-trend-select-dialog",
                "template": options.dialogTemplate || "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${iqtrendselect.settings.dialogTitle}",
                "content-template": null,
                "tab-limit": false,
                "show-close-button": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(options, styleImages, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container settings-content",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "iq-select-box",
                            "config": {
                                "class-name": options.selectStyleClassName || "iq-select-box iq-variant-01 style-select",
                                "template": options.selectStyleTemplate || "default/iq-select-box.iq-variant-01",
                                "name": "styleSelect",
                                "ui": "iq-select-box",
                                "label": "${iqtrendselect.settings.style.label}",
                                "item": null,
                                "auto-label": true,
                                "icon-src": null,
                                "icon-title": null,
                                "icon-class": null,
                                "show-icon": false,
                                "show-text": true,
                                "no-selection-label": "---",
                                "selected": -1,
                                "options": [],
                                "_controllers_": getControllerConfig("selectStyle")
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-checkbox",
                            "config": {
                                "class-name": options.checkFillClassName || "iq-checkbox iq-variant-01 fill-curve",
                                "template": options.checkFillTemplate || "default/iq-checkbox.iq-variant-01",
                                "name": "fillBox",
                                "ui": "iq-checkbox",
                                "label": "${iqtrendselect.settings.fill.label}",
                                "item": null,
                                "auto-label": true,
                                "type": 2,
                                "show-text": true,
                                "show-icon": false,
                                "icon-src": null,
                                "tooltip": null,
                                "icon-class": null,
                                "on-value": true,
                                "off-value": false,
                                "pressed-class": "pressed",
                                "confirm-on": false,
                                "confirm-off": false,
                                "confirm-on-text": "${V_CONFIRM_ON}",
                                "confirm-off-text": "${V_CONFIRM_OFF}",
                                "_controllers_": getControllerConfig("checkFill")
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-checkbox",
                            "config": {
                                "class-name": options.checkToleranceClassName || "iq-checkbox iq-variant-01 draw-tolerance",
                                "template": options.checkToleranceTemplate || "default/iq-checkbox.iq-variant-01",
                                "name": "toleranceBox",
                                "ui": "iq-checkbox",
                                "label": "${iqtrendselect.settings.tolerance.label}",
                                "item": null,
                                "auto-label": true,
                                "type": 2,
                                "show-text": true,
                                "show-icon": false,
                                "icon-src": null,
                                "tooltip": null,
                                "icon-class": null,
                                "on-value": true,
                                "off-value": false,
                                "pressed-class": "pressed",
                                "confirm-on": false,
                                "confirm-off": false,
                                "confirm-on-text": "${V_CONFIRM_ON}",
                                "confirm-off-text": "${V_CONFIRM_OFF}",
                                "_controllers_": getControllerConfig("checkTolerance")
                            },
                            "children": null
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "limitsContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-label",
                                    "config": {
                                        "class-name": options.labelLimitsClassName || "iq-label iq-variant-01 limits-header",
                                        "name": "limitsLabel",
                                        "ui": "iq-label",
                                        "item": null,
                                        "type": null,
                                        "auto-type": true,
                                        "decimal-delimiter": ".",
                                        "precision": 2,
                                        "auto-precision": true,
                                        "options": [],
                                        "pattern": null,
                                        "tooltip": null,
                                        "value-as-tooltip": false,
                                        "text": "${iqtrendselect.settings.limits.header}"
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": options.selectLimitsModeClassName || "iq-select-box iq-variant-01 limits-select",
                                        "template": options.selectLimitsModeTemplate || "default/iq-select-box.iq-variant-01",
                                        "name": "limitModeSelect",
                                        "ui": "iq-select-box",
                                        "label": "${iqtrendselect.settings.limits.mode.label}",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "show-icon": false,
                                        "show-text": true,
                                        "no-selection-label": "---",
                                        "selected": -1,
                                        "options": [
                                            {
                                                "label": "${iqtrendselect.settings.limits.mode.config}",
                                                "value": "config",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            },
                                            {
                                                "label": "${iqtrendselect.settings.limits.mode.auto}",
                                                "value": "auto",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            },
                                            {
                                                "label": "${iqtrendselect.settings.limits.mode.manual}",
                                                "value": "manual",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            }
                                        ],
                                        "_controllers_": getControllerConfig("selectLimitsMode")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.inputLimitsStepClassName || "iq-input-field iq-variant-01 step-input",
                                        "template": options.inputLimitsStepTemplate || "default/iq-input-field.iq-variant-01",
                                        "name": "iq-input-field",
                                        "ui": "iq-input-field",
                                        "label": "${iqtrendselect.settings.limits.step.label}",
                                        "item": null,
                                        "min": 0,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 3,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputLimitsStep")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.inputLimitsMinClassName || "iq-input-field iq-variant-01 min-input",
                                        "template": options.inputLimitsMinTemplate || "default/iq-input-field.iq-variant-01",
                                        "name": "iq-input-field_1",
                                        "ui": "iq-input-field",
                                        "label": "${iqtrendselect.settings.limits.min.label}",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 3,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputLimitsMin")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.inputLimitsMaxClassName || "iq-input-field iq-variant-01 max-input",
                                        "template": options.inputLimitsMaxTemplate || "default/iq-input-field.iq-variant-01",
                                        "name": "iq-input-field_2",
                                        "ui": "iq-input-field",
                                        "label": "${iqtrendselect.settings.limits.max.label}",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 3,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputLimitsMax")
                                    },
                                    "children": null
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "scaleContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-label",
                                    "config": {
                                        "class-name": options.labelScaleClassName || "iq-label iq-variant-01 scale-header",
                                        "name": "scaleLabel",
                                        "ui": "iq-label",
                                        "item": null,
                                        "type": null,
                                        "auto-type": true,
                                        "decimal-delimiter": ".",
                                        "precision": 2,
                                        "auto-precision": true,
                                        "options": [],
                                        "pattern": null,
                                        "tooltip": null,
                                        "value-as-tooltip": false,
                                        "text": "${iqtrendselect.settings.scale.header}"
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": options.selectScaleLocationClassName || "iq-select-box iq-variant-01 location-select list-on-top",
                                        "template": options.selectScaleLocationTemplate || "default/iq-select-box.iq-variant-01",
                                        "name": "locationSelect",
                                        "ui": "iq-select-box",
                                        "label": "${iqtrendselect.settings.scale.location.label}",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "show-icon": false,
                                        "show-text": true,
                                        "no-selection-label": "---",
                                        "selected": -1,
                                        "options": [
                                            {
                                                "label": "${iqtrendselect.settings.scale.location.left}",
                                                "value": "left",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            },
                                            {
                                                "label": "${iqtrendselect.settings.scale.location.right}",
                                                "value": "right",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            }
                                        ],
                                        "_controllers_": getControllerConfig("selectScaleLocation")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-checkbox",
                                    "config": {
                                        "class-name": options.checkScaleHideClassName || "iq-checkbox iq-variant-01 hide-scale",
                                        "template": options.checkScaleHideTemplate || "default/iq-checkbox.iq-variant-01",
                                        "name": "hiddenBox",
                                        "ui": "iq-checkbox",
                                        "label": "${iqtrendselect.settings.scale.hide.label}",
                                        "item": null,
                                        "auto-label": true,
                                        "type": 2,
                                        "show-text": true,
                                        "show-icon": false,
                                        "icon-src": null,
                                        "tooltip": null,
                                        "icon-class": null,
                                        "on-value": true,
                                        "off-value": false,
                                        "pressed-class": "pressed",
                                        "confirm-on": false,
                                        "confirm-off": false,
                                        "confirm-on-text": "${V_CONFIRM_ON}",
                                        "confirm-off-text": "${V_CONFIRM_OFF}",
                                        "_controllers_": getControllerConfig("checkScaleHide")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "container",
                                    "config": {
                                        "class-name": "iq-container",
                                        "type": "iqflex",
                                        "flex-orientation": "row",
                                        "flex-none": true,
                                        "flex-wrap": true,
                                        "flex-primary-align": "start",
                                        "flex-secondary-align": "stretch",
                                        "flex-line-align": "start",
                                        "name": "buttonContainer",
                                        "template": null
                                    },
                                    "children": [
                                        {
                                            "ui": "iq-select-box",
                                            "config": {
                                                "class-name": options.selectScaleGroupClassName || "iq-select-box iq-variant-01 scalegroup-select list-on-top",
                                                "template": options.selectScaleGroupTemplate || "default/iq-select-box.iq-variant-01",
                                                "name": "scaleGroupSelect",
                                                "ui": "iq-select-box",
                                                "label": "${iqtrendselect.settings.scalegroup.label}",
                                                "item": null,
                                                "auto-label": true,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null,
                                                "show-icon": false,
                                                "show-text": true,
                                                "no-selection-label": "---",
                                                "selected": -1,
                                                "options": [],
                                                "_controllers_": getControllerConfig("selectScaleGroup")
                                            },
                                            "children": null
                                        },
                                        {
                                            "ui": "iq-button",
                                            "config": {
                                                "class-name": options.addGroupClassName || "iq-button iq-variant-01 add-scale-group",
                                                "template": options.addGroupTemplate || "default/iq-button.iq-variant-01",
                                                "action": null,
                                                "show-text": true,
                                                "disable-item-lock": false,
                                                "monoflop": false,
                                                "monoflop-value": 1,
                                                "monoflop-interval": 100,
                                                "name": "scaleGroupAdd",
                                                "ui": "iq-button",
                                                "label": "${iqtrendselect.settings.scalegroup.add}",
                                                "item": null,
                                                "auto-label": false,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null,
                                                "label-from-item": false,
                                                "write-bool": false,
                                                "action-release": null,
                                                "action-while-pressed": null,
                                                "interval-while-pressed": null,
                                                "action-press": null,
                                                "disable-alarms": false,
                                                "show-icon": false,
                                                "on-value": true,
                                                "off-value": false,
                                                "tooltip": null,
                                                "_controllers_": getControllerConfig("buttonScaleAddGroup")
                                            },
                                            "children": null
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                },
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container button-container",
                        "type": "iqflex",
                        "flex-orientation": "row",
                        "flex-none": true,
                        "flex-wrap": true,
                        "flex-primary-align": "end",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "buttonContainer",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "iq-button",
                            "config": {
                                "class-name": options.cancelClassName || "iq-button iq-variant-01 cancel",
                                "template": options.cancelTemplate || "default/iq-button.iq-variant-01",
                                "action": null,
                                "show-text": true,
                                "disable-item-lock": false,
                                "monoflop": false,
                                "monoflop-value": 1,
                                "monoflop-interval": 100,
                                "name": "settingsCancel",
                                "ui": "iq-button",
                                "label": "${iqtrendselect.settings.cancel}",
                                "item": null,
                                "auto-label": false,
                                "icon-src": null,
                                "icon-title": null,
                                "icon-class": null,
                                "label-from-item": false,
                                "write-bool": false,
                                "action-release": null,
                                "action-while-pressed": null,
                                "interval-while-pressed": null,
                                "action-press": null,
                                "disable-alarms": false,
                                "show-icon": false,
                                "on-value": true,
                                "off-value": false,
                                "tooltip": null,
                                "_controllers_": getControllerConfig("buttonCancel")
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-button",
                            "config": {
                                "class-name": options.applyClassName || "iq-button iq-variant-01 filled apply",
                                "template": options.applyTemplate || "default/iq-button.iq-variant-01",
                                "action": null,
                                "show-text": true,
                                "disable-item-lock": false,
                                "monoflop": false,
                                "monoflop-value": 1,
                                "monoflop-interval": 100,
                                "name": "settingsApply",
                                "ui": "iq-button",
                                "label": "${iqtrendselect.settings.apply}",
                                "item": null,
                                "auto-label": false,
                                "icon-src": null,
                                "icon-title": null,
                                "icon-class": null,
                                "label-from-item": false,
                                "write-bool": false,
                                "action-release": null,
                                "action-while-pressed": null,
                                "interval-while-pressed": null,
                                "action-press": null,
                                "disable-alarms": false,
                                "show-icon": false,
                                "on-value": true,
                                "off-value": false,
                                "tooltip": null,
                                "_controllers_": getControllerConfig("buttonApply")
                            },
                            "children": null
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create curve configuration dialog
     *
     * @param {object} options curve config dialog options
     * @param {object} options.cellInfo curve information of current table cell
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.selectStyleClassName] class-name to apply to style select iq-selectbox
     * @param {string} [options.selectStyleTemplate] template to use for style select iq-selectbox
     * @param {string} [options.checkFillClassName] class-name to apply to fill curve iq-checkbox
     * @param {string} [options.checkFillTemplate] template to use for fill curve iq-checkbox
     * @param {string} [options.checkToleranceClassName] class-name to apply to draw tolerances iq-checkbox
     * @param {string} [options.checkToleranceTemplate] template to use for draw tolerances iq-checkbox
     * @param {string} [options.labelLimitsClassName] class-name to apply to limits section iq-label
     * @param {string} [options.labelLimitsTemplate] template to use for limits section iq-label
     * @param {string} [options.selectLimitsModeClassName] class-name to apply to limit mode iq-selectbox
     * @param {string} [options.selectLimitsModeTemplate] template to use for limit mode iq-selectbox
     * @param {string} [options.inputLimitsStepClassName] class-name to apply to limit step iq-input-field
     * @param {string} [options.inputLimitsStepTemplate] template to use for limit step iq-input-field
     * @param {string} [options.inputLimitsMinClassName] class-name to apply to limit min iq-input-field
     * @param {string} [options.inputLimitsMinTemplate] template to use for limit min iq-input-field
     * @param {string} [options.inputLimitsMaxClassName] class-name to apply to limit max iq-input-field
     * @param {string} [options.inputLimitsMaxTemplate] template to use for limit max iq-input-field
     * @param {string} [options.labelScaleClassName] class-name to apply to scale section iq-label
     * @param {string} [options.labelScaleTemplate] template to use for scale section iq-label
     * @param {string} [options.selectScaleLocationClassName] class-name to apply to scale location iq-selectbox
     * @param {string} [options.selectScaleLocationTemplate] template to use for scale location iq-selectbox
     * @param {string} [options.checkScaleHideClassName] class-name to apply to hide scale iq-checkbox
     * @param {string} [options.checkScaleHideTemplate] template to use for hide scale iq-checkbox
     * @param {string} [options.selectScaleGroupClassName] class-name to apply to scale group iq-selectbox
     * @param {string} [options.selectScaleGroupTemplate] template to use for scale group iq-selectbox
     * @param {string} [options.addGroupClassName] class-name to apply to add scale group iq-button
     * @param {string} [options.addGroupTemplate] template to use for add scale group iq-button
     * @param {string} [options.applyClassName] class-name to apply to apply settings iq-button
     * @param {string} [options.applyTemplate] template to use for apply settings iq-button
     * @param {string} [options.cancelClassName] class-name to apply to cancel settings iq-button
     * @param {string} [options.cancelTemplate] template to use for cancel settings iq-button
     * @param {string[]} styleImages style preview image urls
     * @return {Promise<object|null>} promise resolving to curve configuration or `null` if closed without selection
     */
    module.getDialog = (options, styleImages = []) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(options, styleImages, resolve, reject);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].show();
                    }
                });
            }
        }
    });
}());
/**
 * Module to create simple, dynamic datagrids to work with small to medium size
 * datasets. Provides an easy way to create custom datagrids by specifying a loader
 * function for row data.
 *
 * @module designer/tools/grid
 */
(function() {
    var MODULE_NAME = "visuals.controls.iqtrendselect.grid",
        /** @lends module:designer/tools/grid */
        module = shmi.pkg(MODULE_NAME);

    /**
     * conjunctive filter mode: all filters must match
     *
     * @constant
     * @type {number}
     * @default
     */
    module.FILTER_MODE_AND = 1;
    /**
     * disjunctive filter mode: at least one filter must match
     *
     * @constant
     * @type {number}
     * @default
     */
    module.FILTER_MODE_OR = 2;

    /**
     * ASCending sort order
     *
     * @constant
     * @type {number}
     * @default
     */
    module.SORT_ORDER_ASC = 1;

    /**
     * DESCending sort order
     *
     * @constant
     * @type {number}
     * @default
     */
    module.SORT_ORDER_DESC = 2;

    /**
     * getColumnSorter - create sorter function for specified column
     *
     * @param {number} rowIndex column index
     *
     * @returns {function} sorter function
     */
    function getColumnSorter(rowIndex) {
        return function columnSorter(a, b) {
            if (!(a[rowIndex] && b[rowIndex])) {
                throw new Error(MODULE_NAME + " invalid row index: " + rowIndex);
            }
            a = String(a[rowIndex].value).toUpperCase();
            b = String(b[rowIndex].value).toUpperCase();

            if (a < b) {
                return -1;
            } else if (a > b) {
                return 1;
            }
            return 0;
        };
    }

    /**
     * sortGridData - apply sorting to grid data
     *
     * the specified array will be modified by the operation and contains the
     * sorted data after execution.
     *
     * @param  {object[]} rows array of row data
     * @param  {number} colIdx column index
     * @param  {number} order sort order, either `module.SORT_ORDER_ASC` or `module.SORT_ORDER_DESC`
     * @return {undefined}
     */
    function sortGridData(rows, colIdx, order) {
        rows.sort(getColumnSorter(colIdx));
        if (order === module.SORT_ORDER_DESC) {
            rows.reverse();
        }
    }

    /**
     * convertFilterToRegExp - create regular expression from filter string
     *
     * @param  {string} filterString input filter string
     * @return {RegExp} created regular expression
     */
    function convertFilterToRegExp(filterString) {
        //convert to lower case since comparison is done against lower cased field value
        filterString = filterString.toLowerCase();
        // Escape any command characters used in regular expressions.
        filterString = filterString.replace(/([.?*{}[\]():^$!])/g, '\\$1');
        // Have any '%' match any substring and '_' match exactly one character.
        filterString = filterString.replace(/%/g, '.*');
        filterString = filterString.replace(/_/g, '.');

        return RegExp('^' + filterString + '$');
    }

    /**
     * filterGridData - filter row data based on specified filter settings
     *
     * does not modify provided array data and instead returns filtered result.
     *
     * @param  {object[]} rows array of row data
     * @param  {string[]} fields field names
     * @param  {object} filter filter settings
     * @param  {number} mode filter mode, either `module.FILTER_MODE_OR` or `module.FILTER_MODE_AND`
     * @return {object[]} filtered row data
     */
    function filterGridData(rows, fields, filter, mode) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject,
            filtered = [],
            re = null,
            noFilterSet = true,
            filterMatches = {};

        iter(filter, function(val) {
            if (val !== null) {
                noFilterSet = false;
            }
        });

        rows.forEach(function(r) {
            iter(filter, function(val, prop) {
                const fieldIndex = fields.indexOf(prop);

                filterMatches[prop] = filterMatches[prop] || [];

                if (noFilterSet) {
                    filterMatches[prop].push(r);
                } else if (val !== null) {
                    re = convertFilterToRegExp(val);
                    if (re && String(r[fieldIndex].value).toLowerCase().match(re)) {
                        filterMatches[prop].push(r);
                    }
                } else if (mode === module.FILTER_MODE_AND) {
                    filterMatches[prop].push(r);
                }
            });
        });

        filtered = rows.filter(function(r) {
            let includeRow = false;

            if (mode === module.FILTER_MODE_OR) {
                includeRow = Object.keys(filter).some(function(fieldName) {
                    return (filterMatches[fieldName].indexOf(r) !== -1);
                });
            } else if (mode === module.FILTER_MODE_AND) {
                includeRow = Object.keys(filter).every(function(fieldName) {
                    return (filterMatches[fieldName].indexOf(r) !== -1);
                });
            }

            return includeRow;
        });

        return filtered;
    }

    /**
     * throwError - shortcut to throw error including module name in text output
     *
     * @param  {string} msg error message
     * @return {undefined}
     */
    function throwError(msg) {
        throw new Error(`[${MODULE_NAME}] ${msg}`);
    }

    /**
     * checkOptions - validate types of grid options
     *
     * @param  {object} gridOptions grid options
     * @return {undefined}
     */
    function checkOptions(gridOptions) {
        const dgm = shmi.requires("visuals.session.DataGridManager");
        if (gridOptions === null || typeof gridOptions !== "object") {
            throwError("parameter 'options' must be an object");
        }

        if (typeof gridOptions.name !== "string" || gridOptions.name.trim().length === 0) {
            throwError("option `name` must be a non-empty string");
        }

        if (dgm.getGrid(gridOptions.name)) {
            throwError(`grid '${gridOptions.name}' already exists`);
        }

        if (!Array.isArray(gridOptions.fields)) {
            throwError("option 'fields' must be an array of field names");
        }

        if (!Array.isArray(gridOptions.index)) {
            throwError("option 'index' must be an array of field names");
        }

        if (typeof gridOptions.mode !== "number" || [module.FILTER_MODE_OR, module.FILTER_MODE_AND].indexOf(gridOptions.mode) === -1) {
            throwError(`option 'mode' must either be '${module.FILTER_MODE_OR}' (OR) or '${module.FILTER_MODE_AND}' (AND)`);
        }
    }

    /**
     * setGridData - insert specified row data into datagrid
     *
     * @param  {object} grid datagrid reference
     * @param  {object[]} rowData array of row data
     * @return {undefined}
     */
    function setGridData(grid, rowData) {
        grid.nextRowId = 0;
        grid.indexMap = {};

        if (grid.totalRows > rowData.length) {
            const diff = grid.totalRows - rowData.length;
            for (let i = 0; i < diff; i++) {
                delete grid.data[rowData.length + i];
            }
        }

        rowData.forEach(function(r, idx) {
            grid.insertRow(r, idx, true);
        });

        grid.totalRows = rowData.length;
        grid._updateSubscriber();
    }

    const loadQueue = [];
    let loadRunning = false;

    function checkQueue() {
        if (loadQueue.length && !loadRunning) {
            loadRunning = true;
            loadQueue[0][0](loadQueue[0][1]);
        }
    }

    /**
     * create - creates a new dynamic datagrid.
     *
     * @param {object} options datargrid options
     * @param {string} options.name name of new datagrid
     * @param {string[]} options.fields datagrid field names
     * @param {string[]} options.index selector field names, must be subset of `options.fields`
     * @param {number} options.mode filter mode, either `module.FILTER_MODE_OR` or `module.FILTER_MODE_AND`
     * @param {function} loadFunction data retrieval function
     *
     * @returns {object} DataGrid reference
     */
    module.create = function create(options, loadFunction) {
        var dgm = shmi.requires("visuals.session.DataGridManager"),
            iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            core = shmi.requires("visuals.core"),
            filter = {},
            sort = null,
            grid = null,
            data = [],
            activeData = [];

        checkOptions(options);
        if (typeof loadFunction !== "function") {
            throwError("parameter 'loadFunction' must be a function accepting a callback parameter to return an array of row data");
        }

        /**
         * onLoaded - callback executed when row data is loaded
         *
         * @param {function} callback callback to run when execution has completed
         * @param {object[]} response row data
         * @return {undefined}
         */
        function onLoaded(callback, response) {
            loadRunning = false;
            loadQueue.splice(0, 1);
            if (!Array.isArray(response)) {
                throwError("callback parameter 'response' must be an array of row data");
            }
            data = response;
            if (sort) {
                sortGridData(data, options.fields.indexOf(sort.column), sort.order);
            }
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
            shmi.decouple(() => {
                checkQueue();
            });
        }

        options.fields.forEach(function(fieldName) {
            filter[fieldName] = null;
        });

        grid = new core.DataGridDynamic(options.name, options.fields, false, null, 0, options.index);

        //called on new subscriptions
        grid.onSubscribe = function onSubscribe(offset, size, callback) {
            loadQueue.push([loadFunction, onLoaded.bind(null, callback)]);
            checkQueue();
        };

        //called when subscriber changes displayed offset
        grid.onSetOffset = function onSetOffset(offset, subId, callback) {
            shmi.raf(callback);
        };

        //called when active filter is changed
        grid.onSetFilter = function onSetFilter(col, expression, callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });
            filter[options.fields[col]] = expression[0];
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
        };

        //called when active filter is cleared
        grid.onClearFilter = function onClearFilter(col, callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });

            if (col === -1) {
                iter(filter, function(val, prop) {
                    filter[prop] = null;
                });
            } else if (options.fields[col]) {
                filter[options.fields[col]] = null;
            }
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
        };

        //called to refresh grid data
        grid.onRefresh = function onRefresh(callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });
            loadQueue.push([loadFunction, onLoaded.bind(null, callback)]);
            checkQueue();
        };

        //called when sort settings are changed
        grid.onSort = function onSort(col, order, callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });
            if (col === -1) {
                sort = null;
            } else {
                sort = {
                    column: options.fields[col],
                    order: (order === "DESC") ? module.SORT_ORDER_DESC : module.SORT_ORDER_ASC
                };
            }

            if (sort) {
                sortGridData(data, options.fields.indexOf(sort.column), sort.order);
            }
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
        };

        //called when grid row is inserted
        grid.onInsert = function onInsert(rowData, rId, noNotify, callback) {
            shmi.raf(callback);
        };

        //called when grid row is deleted
        grid.onDelete = function onDelete(rId, callback) {
            shmi.raf(callback);
        };

        //called when all grid rows are deleted
        grid.onDeleteAll = function onDeleteAll(callback) {
            shmi.raf(callback);
        };

        //called to update subscriber state
        grid._calcState = function _calcState(subId) {
            var self = this;
            var sub = self.subscribers[subId];
            var state = {};
            state.status = "OK";
            state.offset = sub.offset;
            state.totalRows = self.totalRows;

            return state;
        };

        grid.getGridData = () => shmi.cloneObject(activeData);

        dgm.grids[options.name] = grid;

        console.log(MODULE_NAME, "created datagrid:", options.name);

        return grid;
    };

    module.remove = function remove(name) {
        const dgm = shmi.requires("visuals.session.DataGridManager");

        if (shmi.objectHasOwnProperty(dgm.grids, name)) {
            dgm.grids[name].subscribers = {};
            delete dgm.grids[name];
        }
    };
}());

(function() {
    /**
     * module to configure curve settings in iq-trend-select table
     *
     * @module visuals/controls/iqtrendselect/ls/settings.
     *
     */

    const MODULE_NAME = "visuals.controls.iqtrendselect.ls.settings",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        /** @lends module:visuals/controls/iqtrendselect/ls/settings */
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const tokens = [],
            im = shmi.requires("visuals.session.ItemManager"),
            itemHandler = im.getItemHandler();
        let detailsButton = null,
            cellInfo = null;

        self.vars = self.vars || {};

        detailsButton = shmi.createControl("iq-button", self.element.parentNode, {
            "class-name": "iq-button iq-variant-01 curve-settings",
            "template": "default/iq-button.iq-variant-01",
            "label": null,
            "show-icon": true,
            "show-text": false,
            "icon-src": "pics/system/controls/iq-trend-select/edit.svg",
            "inline-svg-icon": true
        }, "DIV");

        itemHandler.setValue = function(value) {
            try {
                cellInfo = JSON.parse(value);
                if (cellInfo && cellInfo.data) {
                    detailsButton.unlock();
                } else {
                    detailsButton.lock();
                    cellInfo = null;
                }
            } catch (exc) {
                cellInfo = null;
            }
        };

        tokens.push(detailsButton.listen("click", function() {
            const { HANDLE_PREFIX } = shmi.requires("visuals.tools.nodes");
            let styleImages = [],
                settingsDialogOptions = {
                    cellInfo
                };
            const selectWidget = self.config.handle ? shmi.ctrl(`${HANDLE_PREFIX}${self.config.handle}`) : null;
            if (selectWidget) {
                styleImages = selectWidget.getStyleImages();
                settingsDialogOptions = Object.assign(selectWidget.config.settingsDialogOptions || {}, settingsDialogOptions);
            }
            shmi.requires("visuals.controls.iqtrendselect.dialog").getDialog(settingsDialogOptions, styleImages || []).then((response) => {
                if (response !== null) {
                    cellInfo.data.options = response;
                    im.writeValue(self.config.item, JSON.stringify(cellInfo));
                }
            });
        }));

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }

        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            shmi.deleteControl(detailsButton);
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();
(function() {
    /**
     * module to preview style for iq-trend-select
     *
     * @module visuals/controls/iqtrendselect/ls/style.
     *
     */

    const MODULE_NAME = "visuals.controls.iqtrendselect.ls.style",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        /** @lends module:visuals/controls/iqtrendselect/ls/style */
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const tokens = [],
            im = shmi.requires("visuals.session.ItemManager"),
            itemHandler = im.getItemHandler();

        self.vars = self.vars || {};

        let imageElement = null;

        itemHandler.setValue = function(value) {
            if (value) {
                if (!imageElement) {
                    imageElement = document.createElement("img");
                }
                imageElement.classList.add("style-image");
                imageElement.setAttribute("src", value);
                self.element.parentNode.appendChild(imageElement);
            } else if (imageElement) {
                imageElement.parentNode.removeChild(imageElement);
                imageElement = null;
            }
        };

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }

        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            if (imageElement) {
                imageElement.parentNode.removeChild(imageElement);
                imageElement = null;
            }
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();
/**
 * WebIQ Visuals widget to select sets of trend values displayed in IQ Trend
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-set-select",
 *     "name": null,
 *     "template": "default/iq-trend-set-select"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default CSS class applied on widget root element
 * name {string}: Name of widget set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.2
 */
(function() {
    'use strict';

    // variables for reference in widget definition
    const className = "IqTrendSetSelect", // widget name in camel case
        uiType = "iq-trend-set-select", // widget keyword (data-ui)
        isContainer = true,
        storageIdentifierSets = "webiq-trend-sets",
        storageIdentifierSettings = "webiq-widget-settings";

    // example - default configuration
    const defaultConfig = {
        "class-name": "iq-trend-set-select",
        "name": null,
        "template": "default/iq-trend-set-select",
        "label": uiType,
        "trend-widget": null,
        "trend-sets-identifier": "trend-sets-1",
        "storage-location": "localStorage",
        "trend-sets": [],
        "selected": null,
        "popup-location": "bottom",
        "modified-marker": "(*)",
        "remember-last-set": false,
        "layoutOptions": {
            "selectBoxClassName": "iq-select-box iq-variant-01 set-select",
            "selectBoxTemplate": "default/iq-select-box.iq-variant-01"
        },
        "fixed-set-icon": "pics/system/controls/iq-trend-set-select/fixed-set.svg"
    };

    // setup module-logger
    const ENABLE_LOGGING = true, //enable logging with 'log' function
        RECORD_LOG = false, //store logged messages in logger memory
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog, //force log - logs message even if logger is disabled
        log = logger.log; //log message if logger is enabled

    // declare private functions - START

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        //trend active
        updateTrendWidget(self);
        self.vars.trendWidgetTokens.push(control.listen("value-info", (evt) => {
            checkModified(self);
        }));
        self.vars.trendWidgetTokens.push(control.listen("graph-options", (evt) => {
            checkModified(self);
        }));
    }

    /**
     * check if selected set is modified
     *
     * @param {object} self control instance
     */
    function checkModified(self) {
        if (self.vars.selected) {
            const trendSet = self.vars.trendSets.find(({ name }) => name === self.vars.selected);
            if (trendSet) {
                const { isEqualDeep } = shmi.requires("visuals.tools.compare");
                if (trendSet.fixed || isEqualDeep(self.vars.trendWidget.getSelectedValues(), trendSet.values)) {
                    setModified(self, false);
                } else {
                    setModified(self, true);
                }
            }
        }
    }

    /**
     * set modified state
     *
     * @param {object} self control instance
     * @param {boolean} modified modified state
     */
    function setModified(self, modified) {
        self.vars.modified = modified;
        if (modified) {
            shmi.addClass(self.element, "modified");
        } else {
            shmi.removeClass(self.element, "modified");
        }
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        //trend inactive
        self.vars.trendWidgetTokens.forEach((t) => t.unlisten());
        self.vars.trendWidgetTokens = [];
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    /**
     * initialize fixed (pre-configured) trend sets
     *
     * @param {object} self control instance
     */
    function initFixedTrendSets(self) {
        self.vars.trendSets = shmi.cloneObject(self.config["trend-sets"]);
        self.vars.trendSets.forEach((trendSet) => {
            trendSet.fixed = true;
        });
    }

    /**
     * save trend sets to localStorage
     *
     * @param {object} self control instance
     */
    function saveTrendSetsLocalStorage(self) {
        const settingsText = localStorage.getItem(storageIdentifierSets),
            username = shmi.requires("visuals.session.UserManager").currentUser.name;
        let trendSetData = {};

        if (settingsText) {
            try {
                trendSetData = JSON.parse(settingsText);
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved selection:`, exc);
            }
        }

        trendSetData[username] = trendSetData[username] || {};
        trendSetData[username][self.vars.trendSetsIdentifier] = self.vars.trendSets.filter((trendSet) => !trendSet.fixed);

        localStorage.setItem(storageIdentifierSets, JSON.stringify(trendSetData, null, 4));
    }

    /**
     * save trend sets to user data
     *
     * @param {object} self control instance
     * @return {Promise} promise resolving when data has been set
     */
    function saveTrendSetsUserData(self) {
        const trendSets = self.vars.trendSets.filter((trendSet) => !trendSet.fixed);
        return new Promise((resolve, reject) => {
            shmi.requires("visuals.session.UserManager").setUserData({
                [`${storageIdentifierSets}:${self.vars.trendSetsIdentifier}`]: JSON.stringify(trendSets, null, 4)
            }, (response, error) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(response);
                }
            });
        });
    }

    /**
     * load trend sets from localStorage
     *
     * @param {object} self control instance
     */
    function loadTrendSetsLocalStorage(self) {
        const settingsText = localStorage.getItem(storageIdentifierSets),
            username = shmi.requires("visuals.session.UserManager").currentUser.name;
        let trendSetData = {};

        if (settingsText) {
            try {
                trendSetData = JSON.parse(settingsText);
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved selection:`, exc);
            }
        }

        trendSetData[username] = trendSetData[username] || {};
        trendSetData[username][self.vars.trendSetsIdentifier] = trendSetData[username][self.vars.trendSetsIdentifier] || [];

        self.vars.trendSets.push(...trendSetData[username][self.vars.trendSetsIdentifier]);
    }

    /**
     * load trend sets from user data
     *
     * @param {object} self control instance
     */
    function loadTrendSetsUserData(self) {
        const settingsText = shmi.requires("visuals.session.UserManager").getUserData(`${storageIdentifierSets}:${self.vars.trendSetsIdentifier}`);
        let trendSetData = [];

        if (settingsText) {
            try {
                trendSetData = JSON.parse(settingsText);
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved selection:`, exc);
            }
        }
        self.vars.trendSets.push(...trendSetData);
    }

    /**
     * load custom trend sets
     *
     * @param {object} self control instance
     */
    function loadTrendSets(self) {
        switch (self.vars.storageLocation) {
        case "localStorage":
            loadTrendSetsLocalStorage(self);
            break;
        case "userData":
            loadTrendSetsUserData(self);
            break;
        default:
            throw new Error(`Invalid storage location configured: ${self.vars.storageLocation}`);
        }
        self.vars.setsLoaded = true;
    }

    /**
     * save custom trend sets
     *
     * @param {object} self control instance
     */
    async function saveTrendSets(self) {
        switch (self.vars.storageLocation) {
        case "localStorage":
            saveTrendSetsLocalStorage(self);
            break;
        case "userData":
            await saveTrendSetsUserData(self);
            break;
        default:
            throw new Error(`Invalid storage location configured: ${self.vars.storageLocation}`);
        }
    }

    /**
     * update values displayed in connected trend widget
     *
     * @param {object} self control instance
     */
    async function updateTrendWidget(self) {
        if (self.vars.trendWidget && self.vars.selected) {
            const selected = self.vars.selected;
            const trendSet = self.vars.trendSets.find(({ name }) => name === self.vars.selected);
            if (trendSet) {
                const widgetInfo = self.vars.trendWidget.getTrendInfo();
                const setItems = trendSet.values.map((value) => value.name);
                if (widgetInfo.type === "trend" && widgetInfo.name) {
                    const tm2 = shmi.requires("visuals.core.TrendManager2");
                    let info = null;
                    try {
                        info = await tm2.getInfo(widgetInfo.name, setItems);
                    } catch (exc) {
                        console.error(`[trend2.info] error retrieving trend info for trend: ${self._name}:`, exc);
                    }
                    const infoItems = info === null ? [] : info.items.map((item) => item.name);
                    trendSet.values = trendSet.values.filter((value) => {
                        const isInTrend = infoItems.includes(value.name);
                        if (!isInTrend) {
                            console.warn(`[${className}] item included in set is not present in trend '${widgetInfo.name}': ${value.name}`);
                        }
                        return isInTrend;
                    });
                }
                //check again if trend widget and selection are still present
                if (self.vars.trendWidget && (selected === self.vars.selected)) {
                    self.vars.trendWidget.setSelectedValues(trendSet.values);
                }
            }
        }
    }

    /**
     * restore last set from local-storage
     *
     * @param {object} self control instance
     */
    function restoreLastSet(self) {
        const settingsText = localStorage.getItem(storageIdentifierSettings);
        if (settingsText) {
            try {
                const username = shmi.requires("visuals.session.UserManager").currentUser.name,
                    widgetSettings = JSON.parse(settingsText);

                let handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self);
                if (!handle) {
                    handle = self.getName(); //use instance name if node handle not available
                }

                if (widgetSettings?.[username]?.[uiType]?.[handle]?.selectedSet) {
                    const trendSet = self.vars.trendSets.find(({ name }) => name === widgetSettings[username][uiType][handle].selectedSet);
                    if (trendSet) {
                        self.vars.selected = trendSet.name;
                    }
                }
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved widget settings:`, exc);
            }
        }
    }

    /**
     * save last set in local-storage
     *
     * @param {object} self control instance
     */
    function saveLastSet(self) {
        if (self.config["remember-last-set"]) {
            const username = shmi.requires("visuals.session.UserManager").currentUser.name,
                settingsText = localStorage.getItem(storageIdentifierSettings);
            let widgetSettings = {};

            let handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self);
            if (!handle) {
                handle = self.getName(); //use instance name if node handle not available
            }

            if (settingsText) {
                try {
                    widgetSettings = JSON.parse(settingsText);
                } catch (exc) {
                    console.error(`[${uiType}] could not parse saved widget settings:`, exc);
                }
            }

            widgetSettings[username] = widgetSettings[username] || {};
            widgetSettings[username][uiType] = widgetSettings[username][uiType] || {};
            widgetSettings[username][uiType][handle] = widgetSettings[username][uiType][handle] || {};
            widgetSettings[username][uiType][handle].selectedSet = shmi.cloneObject(self.vars.selected);

            localStorage.setItem(storageIdentifierSettings, JSON.stringify(widgetSettings));
        }
    }

    /**
     * update options available in set selection box
     *
     * @param {object} control instance
     */
    function updateSetOptions(self) {
        if (self.vars.selectBox) {
            self.vars.ignoreChange = true;
            self.vars.selectBox.setOptions(self.vars.trendSets.map(({ name, fixed }) => ({
                "label": name,
                "tooltip": name,
                "value": name,
                "icon-src": fixed && self.config["fixed-set-icon"] ? self.config["fixed-set-icon"] : null
            })));
            if (!self.vars.selectBox.initialized) {
                const selectedIdx = self.vars.trendSets.findIndex(({ name }) => name === self.vars.selected);
                self.vars.selectBox.config.selected = selectedIdx;
            } else {
                self.vars.selectBox.setValue(self.vars.selected);
            }
            self.vars.ignoreChange = false;
        }
    }

    // declare private functions - END

    // definition of new widget extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defaultConfig,
        /* instance variables */
        vars: {
            elements: {
                selectionAnchor: null
            },
            selectBox: null,
            trendWidget: null,
            trendSetsIdentifier: null,
            selected: null,
            trendSets: [],
            tokens: [],
            trendWidgetTokens: [],
            storageLocation: null,
            setsLoaded: false,
            ignoreChange: false,
            modified: false
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this widget */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.trendSetsIdentifier = self.config["trend-sets-identifier"];
                self.vars.storageLocation = self.config["storage-location"];
                self.vars.selected = self.config.selected ? self.config.selected : null;

                if (self.config["modified-marker"]) {
                    self.element.style.setProperty("--modified-marker-content", `"${self.config["modified-marker"]} "`);
                }

                self.vars.elements.selectionAnchor = shmi.getUiElement("selection-anchor", self.element);
                if (self.vars.elements.selectionAnchor) {
                    self.vars.selectBox = shmi.createControl("iq-select-box", self.vars.elements.selectionAnchor, {
                        "class-name": (self.config.layoutOptions?.selectBoxClassName ?? "iq-select-box iq-variant-01 set-select") + (self.config["popup-location"] === "top" ? " list-on-top" : ""),
                        "template": (self.config.layoutOptions?.selectBoxTemplate ?? "default/iq-select-box.iq-variant-01"),
                        "label": self.config.label,
                        "name": "trendSetSelectBox",
                        "inline-svg": true
                    }, "DIV");
                } else {
                    console.error(`[${className}] template is missing required element ("selection-anchor")`);
                }
            },
            /* called when widget is enabled */
            onEnable: function() {
                const self = this;

                self.controls.forEach((c) => {
                    c.enable();
                });

                initFixedTrendSets(self);
                loadTrendSets(self);

                if (self.config["remember-last-set"]) {
                    restoreLastSet(self);
                }

                if (self.vars.selectBox) {
                    updateSetOptions(self);

                    self.vars.tokens.push(self.vars.selectBox.listen("change", (evt) => {
                        if (self.vars.ignoreChange) {
                            return;
                        }
                        self.vars.selected = evt.detail.value;
                        updateTrendWidget(self);
                        setModified(self, false);
                        saveLastSet(self);
                    }));
                }

                if (self.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(self.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(self, trendWidget);
                    }

                    attachTrendListeners(self);
                }
            },
            /* called when widget is disabled */
            onDisable: function() {
                const self = this;
                self.controls.forEach((c) => {
                    c.disable();
                });
                self.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                self.vars.tokens = [];
                self.vars.trendWidgetTokens.forEach((t) => t.unlisten());
                self.vars.trendWidgetTokens = [];
                self.vars.trendWidget = null;
            },
            /* called when widget is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                if (self.vars.selectBox) {
                    self.vars.selectBox.lock();
                }
            },
            /* called when widget is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                if (self.vars.selectBox) {
                    self.vars.selectBox.unlock();
                }
            },
            /* called by ItemManager when value of subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /* called by ItemManager to provide properties (min & max values etc.) of subscribed item */
            onSetProperties: function(min, max, step, name, type, warnMin, warnMax, prewarnMin, prewarnMax, digits) {

            },
            /* called when widget is deleted - used for instance clean-up */
            onDelete: function() {

            },
            /**
             * get connected trend widget instance
             *
             * @returns {object|null} trend widget instance
            */
            getTrendWidget: function() {
                return this.vars.trendWidget;
            },
            /**
             * get identifier used to store trend sets
             *
             * @returns {string} trend sets identifier
            */
            getTrendSetsIdentifier: function() {
                return this.vars.trendSetsIdentifier;
            },
            /**
             * get list of available trend sets
             *
             * @returns {object[]} trend sets
            */
            getTrendSets: function() {
                return this.vars.setsLoaded ? this.vars.trendSets : null;
            },
            /**
             * get currently selected trend set
             *
             * @returns {object|null} trend set or `null` if none is selected
            */
            getSelected: function() {
                const self = this,
                    selectedSet = self.vars.trendSets.find(({ name }) => name === self.vars.selected);
                return selectedSet ? selectedSet : null;
            },
            /**
             * create new trend set
             *
             * @param {string} setName trend set name
             * @returns {boolean} `true` if trend set was created, `false` else
            */
            createTrendSet: async function(setName) {
                const self = this;
                const trendSet = self.vars.trendSets.find(({ name }) => name === setName);
                if (!trendSet && self.vars.trendWidget) {
                    self.vars.trendSets.push({
                        name: setName,
                        values: self.vars.trendWidget.getSelectedValues()
                    });
                    await saveTrendSets(self);

                    self.vars.selected = setName;
                    setModified(self, false);
                    updateSetOptions(self);
                    saveLastSet(self);

                    return true;
                }
                return false;
            },
            /**
             * rename selected trend set
             *
             * @param {string} setName new trend set name
             * @returns {boolean} `true` if trend set has been renamed, `false` else
             */
            renameTrendSet: async function(setName) {
                const self = this,
                    exists = !self.vars.trendSets.every(({ name }) => name !== setName),
                    trendSet = self.getSelected();

                if (!exists && trendSet && self.vars.trendWidget) {
                    trendSet.name = setName;
                    await saveTrendSets(self);

                    self.vars.selected = setName;
                    checkModified(self);
                    updateSetOptions(self);
                    saveLastSet(self);

                    return true;
                }
                return false;
            },
            /**
             * save currently selected trend set
             *
             * @returns {boolean} `true` if trend set has been saved, `false` else
             */
            saveTrendSet: async function() {
                const self = this;
                const trendSet = self.vars.trendSets.find(({ name, fixed }) => name === self.vars.selected && !fixed);
                if (trendSet && self.vars.trendWidget) {
                    trendSet.values = self.vars.trendWidget.getSelectedValues();
                    await saveTrendSets(self);
                    setModified(self, false);
                    return true;
                }
                return false;
            },
            /**
             * delete currently selected trend set
             *
             * @returns {boolean} `true` if trend set has been deleted, `false` else
             */
            deleteTrendSet: async function() {
                const self = this;

                if (self.vars.selected) {
                    const oldCount = self.vars.trendSets.length;
                    self.vars.trendSets = self.vars.trendSets.filter((trendSet) => (trendSet.name !== self.vars.selected) || trendSet.fixed);
                    if (self.vars.trendSets.length !== oldCount) {
                        self.vars.selected = null;
                        setModified(self, false);
                        updateSetOptions(self);

                        await saveTrendSets(self);
                        saveLastSet(self);

                        return true;
                    }
                }

                return false;
            },
            /**
             * check if currently selected trend set is modified
             *
             * @returns {boolean} `true` if trend set is modified, `false` else
             */
            isTrendSetModified: function() {
                return this.vars.modified;
            }
        }
    };

    // definition of new widget extending BaseControl - END

    // generate widget constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-trend-time
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-time",
 *     "name": null,
 *     "template": "default/iq-trend-time"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-time", // control name in camel-case
        uiType = "iq-trend-time", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-time",
        "name": null,
        "template": "default/iq-trend-time",
        "trend-widget": null,
        "mode": "start-end", //either "start-end", or "start-duration"
        "date-time-format": "$DD.$MM.$YYYY, $HH:$mm:$ss",
        "popup-orientation": "bottom",
        "startLabel": "Start",
        "endLabel": "End",
        "durationLabel": "Duration",
        "layoutOptions": {
            "dateLabel": "Select Date",
            "timeLabel": "Select Time",
            "applyLabel": "OK",
            "cancelLabel": "Cancel"
        }
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * update display start & end time
     *
     * @param {object} self control instance
     */
    function updateDisplayTime(self) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            { startValue, endValue } = self.vars.elements;
        if (self.vars.trendWidget) {
            const dim = self.vars.trendWidget.getDimensionsX();
            if (self.config.mode === "start-end") {
                if (startValue) {
                    startValue.textContent = formatDateTime(Math.floor(dim.min / 1000), shmi.localize(self.config["date-time-format"]));
                }
                if (endValue) {
                    endValue.textContent = formatDateTime(Math.floor(dim.max / 1000), shmi.localize(self.config["date-time-format"]));
                }
            } else if (self.config.mode === "start-duration") {
                if (startValue) {
                    startValue.textContent = formatDateTime(dim.min / 1000, shmi.localize(self.config["date-time-format"]));
                }
            }
        }
    }

    /**
     * update inactive-mode state of trend widget
     *
     * @param {object} self control instance
     */
    function updateInactiveState(self) {
        self.vars.isInactive = !self.vars.trendWidget;
        if (self.vars.isInactive) {
            shmi.addClass(self.element, "inactive-mode");
            if (self.vars.durationSelect) {
                self.vars.durationSelect.lock();
            }
        } else {
            shmi.removeClass(self.element, "inactive-mode");
            if (self.vars.durationSelect) {
                self.vars.durationSelect.unlock();
            }
        }
    }

    /**
     * wait for control to initialize
     *
     * @returns {Promise} promise resolving when control is initialized
    */
    function onInitControl(control) {
        return new Promise((resolve) => {
            shmi.waitOnInit(control, resolve);
        });
    }

    /**
     * check if widget is completely initialized
     *
     * @param {object} self control instance
     * @return {boolean} `true` if widget is ready, `false` else
     */
    function isWidgetReady(self) {
        return self.isActive() && self.vars.trendWidget && self.vars.durationSelect;
    }

    /**
     * update selectable durations
     *
     * @param {object} self control instance
     */
    async function updateDurationOptions(self) {
        if (isWidgetReady(self)) {
            await onInitControl(self.vars.durationSelect);
            if (!isWidgetReady(self)) {
                return;
            }
            const { formatDuration } = shmi.requires("visuals.tools.date");
            const options = self.vars.trendWidget.getDisplayDurations().reverse().map((duration) => ({
                label: formatDuration(duration, "compact"),
                value: duration
            }));
            self.vars.ignoreChange = true;
            self.vars.durationSelect.setOptions(options);
            const currentDuration = self.vars.trendWidget.getDisplayDuration();
            self.vars.durationSelect.setValue(currentDuration);
            self.vars.ignoreChange = false;
        }
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        updateDisplayTime(self);
        updateInactiveState(self);
        updateDurationOptions(self);

        self.vars.tokens.push(control.listen("live", (evt) => {
            if (!evt.detail.isLive) {
                updateDisplayTime(self);
            }
        }));

        self.vars.tokens.push(control.listen("display-duration", (evt) => {
            updateDisplayTime(self);
            if (self.vars.durationSelect) {
                self.vars.ignoreChange = true;
                self.vars.durationSelect.setValue(evt.detail.duration);
                self.vars.ignoreChange = false;
            }
        }));

        self.vars.tokens.push(control.listen("display-time", updateDisplayTime.bind(null, self)));

        if (!self.isLocked()) {
            shmi.removeClass(self.element, "locked");
        }
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;

        updateInactiveState(self);
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                startTime: null,
                startValue: null,
                endTime: null,
                endValue: null,
                duration: null
            },
            durationSelect: null,
            ignoreChange: false,
            tokens: [],
            listeners: [],
            isInactive: false,
            templateElementsFound: false
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                    { elements } = self.vars;

                elements.startTime = shmi.getUiElement("start-time", self.element);
                elements.startLabel = shmi.getUiElement("start-label", self.element);
                elements.startValue = elements.startTime ? shmi.getUiElement("label", elements.startTime) : null;
                elements.endTime = shmi.getUiElement("end-time", self.element);
                elements.endLabel = shmi.getUiElement("end-label", self.element);
                elements.endValue = elements.endTime ? shmi.getUiElement("label", elements.endTime) : null;
                elements.duration = shmi.getUiElement("duration", self.element);

                if (elements.startTime && elements.startValue && elements.startLabel && elements.endTime && elements.endValue && elements.endLabel && elements.duration) {
                    self.vars.templateElementsFound = true;
                } else {
                    console.error(`[${uiType}] template is missing required elements: ${self.config.template}`);
                    return;
                }

                elements.startLabel.textContent = shmi.localize(self.config.startLabel);
                elements.endLabel.textContent = shmi.localize(self.config.endLabel);

                if (elements.startTime) {
                    const handler = {
                        onClick: async () => {
                            if (self.vars.trendWidget) {
                                const dim = self.vars.trendWidget.getDimensionsX(),
                                    result = await shmi.visuals.tools.date.requestDateAndTime(Object.assign(self.config.layoutOptions || {}, {
                                        timestamp: dim.max
                                    }));
                                if (result !== null) {
                                    self.vars.trendWidget.setOptions({
                                        tStart: result
                                    });
                                    if (self.config.mode === "start-end") {
                                        const duration = Math.floor(dim.max - result);
                                        self.vars.trendWidget.setDisplayDuration(duration >= 1000 ? duration : 1000);
                                    }
                                }
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(elements.startTime, handler),
                        new TouchListener(elements.startTime, handler)
                    );
                }

                if (self.config.mode === "start-end") {
                    shmi.addClass(self.element, "mode-start-end");
                    if (elements.endTime) {
                        const handler = {
                            onClick: async () => {
                                if (self.vars.trendWidget) {
                                    const dim = self.vars.trendWidget.getDimensionsX(),
                                        result = await shmi.visuals.tools.date.requestDateAndTime(Object.assign(self.config.layoutOptions || {}, {
                                            timestamp: dim.max
                                        }));
                                    if (result !== null) {
                                        const duration = Math.floor(result - dim.min);
                                        if (duration < 1000) {
                                            self.vars.trendWidget.setOptions({
                                                tStart: result - 1000
                                            });
                                        } else {
                                            self.vars.trendWidget.setOptions({
                                                tStart: dim.min
                                            });
                                        }
                                        self.vars.trendWidget.setDisplayDuration(duration >= 1000 ? duration : 1000);
                                    }
                                }
                            }
                        };
                        self.vars.listeners.push(
                            new MouseListener(elements.endTime, handler),
                            new TouchListener(elements.endTime, handler)
                        );
                    }
                } else if (self.config.mode === "start-duration") {
                    shmi.addClass(self.element, "mode-start-duration");
                    if (elements.duration) {
                        const { layoutOptions } = self.config,
                            durationClassName = layoutOptions && layoutOptions.selectDurationClassName ? layoutOptions.selectDurationClassName : `iq-select-box iq-variant-02${["top"].includes(self.config["popup-orientation"]) ? " list-on-top": ""}`,
                            durationTemplate = layoutOptions && layoutOptions.selectDurationTemplate ? layoutOptions.selectDurationTemplate : "default/iq-select-box.iq-variant-02";
                        self.vars.durationSelect = shmi.createControl("iq-select-box", elements.duration, {
                            "label": self.config.durationLabel,
                            "class-name": durationClassName,
                            "template": durationTemplate
                        });
                        self.vars.tokens.push(self.vars.durationSelect.listen("change", (evt) => {
                            if (self.vars.trendWidget && !self.vars.ignoreChange) {
                                self.vars.trendWidget.setDisplayDuration(evt.detail.value);
                            }
                        }));
                    }
                }
            },
            /* Called when control is enabled */
            onEnable: function() {
                if (!this.vars.templateElementsFound) {
                    return;
                }

                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
                updateInactiveState(this);
                this.controls.forEach((c) => {
                    c.enable();
                });
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            },
            /* Called when control is disabled */
            onDisable: function() {
                if (!this.vars.templateElementsFound) {
                    return;
                }
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    if (t.name !== "change") { //keep duration change listener token
                        t.unlisten();
                    }
                });
                this.vars.tokens = this.vars.tokens.filter((t) => t.name === "change");
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
                this.controls.forEach((c) => {
                    c.disable();
                });
            },
            /* Called when control is deleted */
            onDelete: function() {
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
            },
            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                if (!this.vars.templateElementsFound) {
                    return;
                }
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
                if (this.vars.durationSelect) {
                    this.vars.durationSelect.lock();
                }
            },
            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.trendWidget) {
                    shmi.removeClass(this.element, 'locked');
                }
                if (!this.vars.templateElementsFound) {
                    return;
                }
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
                if (this.vars.durationSelect) {
                    this.vars.durationSelect.unlock();
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend-zoom
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-zoom",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-zoom"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-zoom", // control name in camel-case
        uiType = "iq-trend-zoom", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-zoom",
        "name": null,
        "template": "default/iq-trend-zoom",
        "trend-widget": null,
        "orientation": "horizontal"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        if (!self.isLocked()) {
            shmi.removeClass(self.element, "locked");
        }
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        if (self.isActive() && !self.isLocked()) {
            shmi.addClass(self.element, "locked");
        }
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                inButton: null,
                outButton: null
            },
            tokens: [],
            listeners: []
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { MouseListener, TouchListener } = shmi.requires("visuals.io");

                self.vars.elements.inButton = shmi.getUiElement("in-button", self.element);
                self.vars.elements.outButton = shmi.getUiElement("out-button", self.element);

                if (self.config.orientation === "vertical") {
                    shmi.addClass(self.element, "vertical");
                }

                if (self.vars.elements.inButton && self.vars.elements.outButton) {
                    const inHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget) {
                                if (self.config.axis === "y-axis") {
                                    self.vars.trendWidget.zoomInY();
                                } else if (self.config.axis === "x-axis") {
                                    self.vars.trendWidget.zoomInX();
                                }
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.inButton, inHandler),
                        new TouchListener(self.vars.elements.inButton, inHandler)
                    );

                    const outHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget) {
                                if (self.config.axis === "y-axis") {
                                    self.vars.trendWidget.zoomOutY();
                                } else if (self.config.axis === "x-axis") {
                                    self.vars.trendWidget.zoomOutX();
                                }
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.outButton, outHandler),
                        new TouchListener(self.vars.elements.outButton, outHandler)
                    );
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                const { elements } = this.vars;
                if (!(elements.inButton && elements.outButton)) {
                    console.error(`[${uiType}] template is missing required elements.`);
                    return;
                }
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            },

            /* Called when control is disabled */
            onDisable: function() {
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is deleted */
            onDelete: function() {

            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqTrend", // control name in camel-case
        uiType = "iq-trend", // control keyword (data-ui)
        isContainer = true;

    /* default graph value style */
    const defaultValueStyle = {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(0, 0, 0)",
                fillStyle: "rgba(0, 0, 0, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(0, 0, 0)",
                lineDash: [5, 5]
            }
        },
        /* default inactive graph value style (not highlighted) */
        defaultInactiveStyle = {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(163, 163, 163)",
                fillStyle: "rgba(163, 163, 163, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(163, 163, 163)",
                lineDash: [5, 5]
            }
        };

    const storageIdentifier = "webiq-widget-settings";

    /* default list of graph value styles */
    const valueStyles = [
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(141, 77, 163)",
                fillStyle: "rgba(141, 77, 163, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(141, 77, 163)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(227, 67, 147)",
                fillStyle: "rgba(227, 67, 147, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(227, 67, 147)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(211, 229, 116)",
                fillStyle: "rgba(211, 229, 116, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(211, 229, 116)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(71, 178, 178)",
                fillStyle: "rgba(71, 178, 178, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(71, 178, 178)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(186, 74, 130)",
                fillStyle: "rgba(186, 74, 130, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(186, 74, 130)",
                lineDash: [5, 5]
            }
        }
    ];

    // default configuration
    const defConfig = {
        "class-name": "iq-trend",
        "name": null,
        "template": "default/iq-trend",
        "tooltip": null,
        "datasource": {
            "type": "trend",
            "name": "Trend"
        },
        "valueStyles": valueStyles,
        "defaultValueStyle": defaultValueStyle,
        "inactiveStyle": defaultInactiveStyle,
        "selectedValues": [],
        "displayDuration": 60000,
        "availableDurations": null,
        "availableZoomStages": null,
        "timelineMarkerFormats": null,
        "timelineStartMarkerFormat": null,
        "handleDateTimeFormat": {
            "date": "$DD.$MM.$YYYY",
            "time": "$HH:$mm:$ss",
            "time_sub_second": "$HH:$mm:$ss .${SSS}"
        },
        "handle": {
            "enabled": true,
            "secondary": true
        },
        "showComments": true,
        "startLive": false,
        "rememberSelectedValues": false,
        "noValueText": "<no value>",
        "commentTimestampFormat": "$DD.$MM.$YYYY, $HH:$mm:$ss",
        "commentButtonConfig": {
            "class-name": "iq-button iq-variant-01",
            "template": "default/iq-button.iq-variant-01",
            "action": null,
            "show-text": true,
            "disable-item-lock": false,
            "monoflop": false,
            "monoflop-value": 1,
            "monoflop-interval": 100,
            "name": "iq-button",
            "ui": "iq-button",
            "label": "Comment",
            "item": null,
            "auto-label": true,
            "icon-src": null,
            "icon-title": null,
            "icon-class": null,
            "label-from-item": false,
            "write-bool": false,
            "action-release": null,
            "action-while-pressed": null,
            "interval-while-pressed": null,
            "action-press": null,
            "disable-alarms": false,
            "show-icon": false,
            "on-value": 1,
            "off-value": 0,
            "tooltip": null
        }
    };

    /* default zoom stages (y-axis) */
    const zoomStages = [
        /* [0] */ 100,
        /* [1] */ 110,
        /* [2] */ 125,
        /* [3] */ 150,
        /* [4] */ 175,
        /* [5] */ 200,
        /* [6] */ 250,
        /* [7] */ 300,
        /* [8] */ 400,
        /* [9] */ 500,
        /* [10] */ 600,
        /* [11] */ 700,
        /* [12] */ 800,
        /* [13] */ 900,
        /* [14] */ 1000
    ];

    /* definition of time units */
    const time = {
        SECOND: 1000,
        MINUTE: 60000,
        HOUR: 3600000,
        DAY: 86400000,
        WEEK: 604800000,
        MONTH: 2592000000,
        YEAR: 31536000000
    };

    /* default display durations (x-axis) */
    const displayDurations = [
        time.SECOND,
        2 * time.SECOND,
        3 * time.SECOND,
        4 * time.SECOND,
        5 * time.SECOND,
        10 * time.SECOND,
        30 * time.SECOND,
        time.MINUTE,
        2 * time.MINUTE,
        3 * time.MINUTE,
        5 * time.MINUTE,
        10 * time.MINUTE,
        15 * time.MINUTE,
        30 * time.MINUTE,
        time.HOUR,
        2 * time.HOUR,
        4* time.HOUR,
        8 * time.HOUR,
        12 * time.HOUR,
        time.DAY,
        2 * time.DAY,
        time.WEEK,
        2 * time.WEEK,
        time.MONTH,
        3 * time.MONTH,
        6 * time.MONTH,
        time.YEAR
    ];

    /* pixel/value density */
    const pixelsPerValue = 2;

    /**
     * extract values from datasource data
     *
     * @param {object} data datasource data
     * @param {string} itemName item name
     * @return {any[][]} array of timestamp, value, property tuples
     */
    function getSourceDataValues(data, itemName) {
        const values = [],
            itemData = data.values[itemName];

        Object.keys(itemData).forEach((property) => {
            values.push(...itemData[property].map(([timestamp, value]) => (property === "item" ? [timestamp, value, null] : [timestamp, value, property])));
        });

        return values;
    }

    /**
     * update highlighting state of datasets
     *
     * @param {object} self instance reference
     */
    function updateHighlightState(self) {
        const { selectedValues, datasets } = self.vars,
            styles = self.getValueStyles(),
            highlightOption = selectedValues.find((selected) => selected.options && selected.options.highlight);

        datasets.forEach((ds, idx) => {
            if (!highlightOption || highlightOption.name === ds.name) {
                ds.setOptions({
                    style: styles[selectedValues[idx].options.style] ? styles[selectedValues[idx].options.style] : self.config.defaultValueStyle
                });
            } else {
                ds.setOptions({
                    style: self.config.inactiveStyle
                });
            }
            ds.update();
        });
    }

    /**
     * add value to graph display
     *
     * @param {object} self instance reference
     * @param {string} itemName item name
     * @param {object} [options={
     *         drawTolerance: false,
     *         externalDraw: true,
     *         fillCurve: true,
     *         style: 0,
     *         scaleGroup: null,
     *         limits: null
     *     }] value options
     * @param {boolean} [options.drawTolerance] set to `true` to display tolerances
     * @param {boolean} [options.externalDraw] set to `true` to manually trigger graph updates
     * @param {boolean} [options.fillCurve] set to `true` to fill displayed graph curve
     * @param {number} [options.style] configured style index
     * @param {number|null} [options.scaleGroup] scale group ID or `null` if not bound to scale group
     * @param {object|null} [options.limits] limit options
     * @return {object} graph value handling reference
     */
    function addGraphValue(self, itemName, options = {
        drawTolerance: false,
        externalDraw: true,
        fillCurve: true,
        style: 0,
        scaleGroup: null,
        limits: null
    }) {
        options = shmi.cloneObject(options); //create copy to not modify original input parameter
        const { DataSet } = shmi.requires("visuals.controls.iqtrend.dataset");
        const styleConfig = self.config.valueStyles[options.style] || self.config.defaultValueStyle;
        options.scaleGroup = typeof options.scaleGroup === "undefined" ? null : options.scaleGroup;
        options.scale = options.scale || {
            location: options.scale && options.scale.location && ["left", "right"].includes(options.scale.location) ? options.scale.location : self.vars.elements.scalesLeft.childElementCount > self.vars.elements.scalesRight.childElementCount ? "right": "left"
        };
        options.style = styleConfig;
        options.name = itemName;
        const dataset = new DataSet(self.vars.elements, options, self);

        const xDims = self.vars.xAxis.getDimensions();
        const tsStart = xDims.min;
        const tsEnd = xDims.max;
        const duration = (tsEnd - tsStart);
        const resolution = Math.ceil((duration / dataset.getDimensions().width) * pixelsPerValue);

        dataset.setOptions({
            x: {
                min: tsStart,
                max: tsEnd
            },
            y: {
                min: 0,
                max: 1
            },
            interval: resolution
        });

        updateZoomStage(self, self.vars.focusY, dataset);
        let limitsOption = null;
        if (options.limits) {
            limitsOption = shmi.cloneObject(options.limits);
            limitsOption.name = options.name;
        }

        let toleranceOption = null;
        if (typeof options.drawTolerance === "boolean") {
            toleranceOption = {
                name: options.name,
                drawTolerance: options.drawTolerance
            };
        }

        self.vars.datasource.source.setOptions({
            tStart: tsStart,
            duration,
            interval: resolution,
            limits: limitsOption ? limitsOption : undefined,
            tolerance: toleranceOption ? toleranceOption : undefined,
            loadComments: self.config.showComments
        }, true);
        if (self.vars.markerLayer) {
            self.vars.markerLayer.setOptions({
                x: {
                    min: tsStart,
                    max: tsStart + duration
                }
            });
        }
        let setUpdateTimeout = 0;
        const setToken = dataset.watch(({ width, dimensions }) => {
            clearTimeout(setUpdateTimeout);
            setUpdateTimeout = setTimeout(() => {
                if (self.vars.handleInfo.primary.active) {
                    if (self.vars.handleInfo.primary.graphWidth !== width) {
                        const oldWidth = self.vars.handleInfo.primary.graphWidth,
                            scaleFactor = (oldWidth + 1) / (width + 1);
                        self.vars.handleInfo.primary.graphWidth = width;
                        self.vars.handleInfo.primary.offset /= scaleFactor;
                        self.vars.elements.handle.primary.base.style.left = `${self.vars.handleInfo.primary.offset}px`;
                        setHandlePosition(self, true, self.vars.handleInfo.primary.offset, false);
                    }
                }
                if (self.vars.handleInfo.secondary.active) {
                    if (self.vars.handleInfo.secondary.graphWidth !== width) {
                        const oldWidth = self.vars.handleInfo.secondary.graphWidth,
                            scaleFactor = (oldWidth + 1) / (width + 1);
                        self.vars.handleInfo.secondary.graphWidth = width;
                        self.vars.handleInfo.secondary.offset /= scaleFactor;
                        self.vars.elements.handle.secondary.base.style.left = `${self.vars.handleInfo.secondary.offset}px`;
                        setHandlePosition(self, false, self.vars.handleInfo.secondary.offset, false);
                    }
                }
                self.vars.datasource.source.setOptions({
                    interval: Math.ceil(((dimensions.x.max - dimensions.x.min) / width) * pixelsPerValue)
                });

                if (self.vars.markerLayer) {
                    self.vars.markerLayer.updateSize();
                }
            }, 100);
        });

        return {
            setToken,
            dataset
        };
    }

    /**
     * update zoomstage for dataset
     *
     * @param {object} self instance reference
     * @param {number} focusY focus y-coordinate
     * @param {object} ds dataset reference
     */
    function updateDatasetZoomStage(self, focusY, ds) {
        if (!self.vars.datasource.source.getValueSettings()[ds.name]) {
            return;
        }
        if (ds.scaleGroup && ds.scaleGroup.master.dataset !== ds) {
            return;
        }
        const { min, max } = ds.scaleGroup ? ds.scaleGroup.getValueLimits() : self.vars.datasource.source.getValueSettings()[ds.name];
        if (self.vars.zoomStage === 0) {
            if (ds.scaleGroup) {
                ds.scaleGroup.setOptions({
                    y: {
                        min,
                        max
                    }
                }, null);
            } else {
                ds.setOptions({
                    y: {
                        min,
                        max
                    }
                });
            }
            return;
        }
        const range = max - min;
        const newRange = range / (self.vars.zoomStages[self.vars.zoomStage] / 100);
        const focusValue = ds.getValueAtY(focusY),
            currentDim = ds.getDimensions().y,
            currentRange = currentDim.max - currentDim.min,
            valueScale = (focusValue - currentDim.min) / currentRange;
        let newMin = focusValue - (valueScale * newRange),
            newMax = newMin + newRange;

        if (newMin < min) {
            const diff = min - newMin;
            newMin = newMin + diff;
            newMax = newMin + newRange;
        } else if (newMax > max) {
            const diff = newMax - max;
            newMax = newMax - diff;
            newMin = newMax - newRange;
        }

        if (ds.scaleGroup) {
            ds.scaleGroup.setOptions({
                y: {
                    min: newMin,
                    max: newMax
                }
            }, null);
        } else {
            ds.setOptions({
                y: {
                    min: newMin,
                    max: newMax
                }
            });
        }
    }

    /**
     * update zoomstage for all or specified dataset
     *
     * @param {object} self instance reference
     * @param {number} focusY focus y-coordinate
     * @param {object|null} [dataset=null] dataset reference, if `null` all datasets are updated
     */
    function updateZoomStage(self, focusY, dataset = null) {
        if (dataset !== null) {
            updateDatasetZoomStage(self, focusY, dataset);
        } else {
            self.vars.datasets.forEach((ds) => {
                updateDatasetZoomStage(self, focusY, ds);
            });
        }
    }

    /**
     * get dataset limits
     *
     * @param {object} self instance reference
     * @param {object} dataset dataset reference
     * @return {object} dataset limits
     */
    function getMinMax(self, dataset) {
        if (dataset.scaleGroup) {
            const minValues = dataset.scaleGroup.getValueNames().map((name) => self.vars.datasource.source.getValueSettings()[name].min);
            const maxValues = dataset.scaleGroup.getValueNames().map((name) => self.vars.datasource.source.getValueSettings()[name].max);
            return {
                min: Math.min(...minValues),
                max: Math.max(...maxValues)
            };
        } else {
            const valueSettings = self.vars.datasource.source.getValueSettings()[dataset.name];
            return {
                min: valueSettings.min,
                max: valueSettings.max
            };
        }
    }

    /**
     * update drag position
     *
     * @param {object} self instance reference
     * @param {number} dx change of x-coordinate
     * @param {number} dy change of y-coordinate
     */
    function updateDrag(self, dx, dy) {
        if (dy !== 0) {
            self.vars.datasets.forEach((ds) => {
                let setScaleGroup = false;
                if (ds.scaleGroup && ds.scaleGroup.master && ds.scaleGroup.master.dataset !== ds) {
                    return;
                } else if (ds.scaleGroup && ds.scaleGroup.master) {
                    setScaleGroup = true;
                }
                const { min, max } = getMinMax(self, ds);
                const { scaleY } = ds.graph;
                const currentDim = ds.getDimensions().y,
                    range = currentDim.max - currentDim.min;
                const diff = dy / scaleY;
                if (dy > 0 && currentDim.max !== max) { //drag up
                    let newMax = currentDim.max + diff;
                    if (newMax > max) {
                        newMax = max;
                    }
                    if (setScaleGroup) {
                        ds.scaleGroup.setOptions({
                            y: {
                                min: newMax - range,
                                max: newMax
                            }
                        }, null);
                    } else {
                        ds.setOptions({
                            y: {
                                min: newMax - range,
                                max: newMax
                            }
                        });
                    }
                } else if (dy < 0 && currentDim.min !== min) {
                    let newMin = currentDim.min + diff;
                    if (newMin < min) {
                        newMin = min;
                    }
                    if (setScaleGroup) {
                        ds.scaleGroup.setOptions({
                            y: {
                                min: newMin,
                                max: newMin + range
                            }
                        }, null);
                    } else {
                        ds.setOptions({
                            y: {
                                min: newMin,
                                max: newMin + range
                            }
                        });
                    }
                }
            });
        }

        if (dx !== 0) {
            if (self.vars.live) {
                self.setLive(false);
            }
            const min = 0,
                max = Date.now(),
                { xAxis } = self.vars,
                scaleX = xAxis.getScale(),
                currentDim = xAxis.getDimensions(),
                range = currentDim.max - currentDim.min,
                diff = dx / scaleX;
            let newMax = currentDim.max,
                newMin = currentDim.min;

            if (dx < 0 && currentDim.max !== max) {
                newMax = currentDim.max - diff;
                if (newMax > max) {
                    newMax = max;
                }
                newMin = newMax - range;
            } else if (dx > 0 && currentDim.min !== min) {
                newMin = currentDim.min - diff;
                if (newMin < min) {
                    newMin = min;
                }
                newMax = newMin + range;
            }

            if ((newMin !== currentDim.min) || (newMax !== currentDim.max)) {
                xAxis.setOptions({
                    x: {
                        min: newMin,
                        max: newMax
                    }
                });
                self.vars.datasets.forEach((ds) => {
                    ds.setOptions({
                        x: {
                            min: newMin,
                            max: newMax
                        }
                    },
                    true);
                });
                if (self.vars.markerLayer) {
                    self.vars.markerLayer.setOptions({
                        x: {
                            min: newMin,
                            max: newMax
                        }
                    });
                }
                self.vars.datasource.source.setOptions({
                    tStart: newMin
                });
            }
        }
    }

    /**
     * update duration displayed on graph
     *
     * @param {object} self instance reference
     * @param {number} displayDuration displayed duration in ms
     * @param {number} focusX focus x-coordinate
     */
    function updateDisplayDuration(self, displayDuration, focusX) {
        const ds0 = self.vars.datasets[0],
            max = Date.now(),
            min = 0,
            focusTime = Math.round(ds0.getTimeAt(focusX)),
            currentDim = ds0.getDimensions().x,
            currentRange = currentDim.max - currentDim.min,
            valueScale = (focusTime - currentDim.min) / currentRange;
        let newMin = focusTime - (valueScale * displayDuration),
            newMax = newMin + displayDuration;

        if (newMin < min) {
            let diff = min - newMin;
            newMin = newMin + diff;
            newMax = newMin + displayDuration;
        } else if (newMax > max) {
            let diff = newMax - max;
            newMax = newMax - diff;
            newMin = newMax - displayDuration;
        }
        self.vars.xAxis.setOptions({
            x: {
                min: newMin,
                max: newMax
            }
        });
        self.vars.datasets.forEach((g) => {
            g.setOptions({
                x: {
                    min: newMin,
                    max: newMax
                }
            },
            true);
        });
        if (self.vars.markerLayer) {
            self.vars.markerLayer.setOptions({
                x: {
                    min: newMin,
                    max: newMax
                }
            });
        }
        self.vars.xAxis.draw();
        self.vars.datasets.forEach((ds) => {
            ds.update();
        });
        const dimensions = ds0.getDimensions();
        self.vars.datasource.source.setOptions({
            tStart: newMin,
            duration: newMax - newMin,
            interval: Math.ceil(((dimensions.x.max - dimensions.x.min) / dimensions.width) * pixelsPerValue)
        });
    }

    /**
     * update time displayed on trend handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     */
    function updateHandleTime(self, primary) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;

        if (self.vars.datasets.length) {
            const timestamp = self.vars.datasets[0].getTimeAt(handleInfo.offset);
            handleInfo.timestamp = timestamp;
        }

        const timeFormat = self.vars.datasets.length && self.vars.datasets[0].graph.interval < 1000 ? shmi.localize(self.config.handleDateTimeFormat.time_sub_second) : shmi.localize(self.config.handleDateTimeFormat.time);
        handleRef.date.textContent = formatDateTime(handleInfo.timestamp / 1000, shmi.localize(self.config.handleDateTimeFormat.date));
        handleRef.time.textContent = formatDateTime(handleInfo.timestamp / 1000, timeFormat);
    }

    /**
     * check if unit is a unit-class
     *
     * @param {string|number} unitText configured unit
     * @return {boolean} `true` if unit is unit-class, `false` else
     */
    function isUnitClass(unitText) {
        return String(parseInt(unitText)) === String(unitText);
    }

    /**
     * update values displayed on trend-handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     */
    function updateHandleValues(self, primary) {
        const { formatNumber } = shmi.requires("visuals.tools.numericValues"),
            valueSettings = self.vars.datasource.source.getValueSettings(),
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary,
            valueInfo = getHandleValues(self, handleInfo.offset),
            uc = shmi.requires("visuals.tools.unitClasses");

        Object.keys(valueInfo).forEach((name) => {
            const dataset = self.vars.datasets.find((ds) => ds.name === name);
            if (!handleInfo.values[name]) {
                const ref = {
                    base: self.vars.handleValuesTemplate.cloneNode(true),
                    label: null,
                    value: null,
                    unit: null
                };

                ref.label = shmi.getUiElement("label", ref.base);
                ref.value = shmi.getUiElement("value", ref.base);
                ref.unit = shmi.getUiElement("unit", ref.base);
                handleInfo.values[name] = {
                    ref,
                    info: valueInfo[name],
                    strokeStyle: null,
                    labelText: null
                };

                handleRef.values.appendChild(ref.base);

                if (dataset) {
                    handleInfo.values[name].strokeStyle = dataset.graph.style.value.strokeStyle;
                    ref.base.style.setProperty("--line-color", dataset.graph.style.value.strokeStyle);
                }
            } else if (dataset && handleInfo.values[name].strokeStyle !== dataset.graph.style.value.strokeStyle) {
                handleInfo.values[name].strokeStyle = dataset.graph.style.value.strokeStyle;
                handleInfo.values[name].ref.base.style.setProperty("--line-color", dataset.graph.style.value.strokeStyle);
            }

            const labelText = valueSettings[name] && valueSettings[name].label ? shmi.localize(valueSettings[name].label) : name;
            if (handleInfo.values[name].labelText !== labelText) {
                handleInfo.values[name].labelText = labelText;
                handleInfo.values[name].ref.label.textContent = labelText;
            }

            let unitText = "";
            if (valueSettings[name]) {
                if (isUnitClass(valueSettings[name].unit)) {
                    const ucAdapter = uc.getSelectedAdapter(valueSettings[name].unit);
                    if (ucAdapter) {
                        unitText = ucAdapter.unitText;
                    }
                } else {
                    unitText = valueSettings[name].unit;
                }
            }

            if (valueInfo[name].value === null) {
                handleInfo.values[name].ref.value.textContent = self.config.noValueText ? shmi.localize(self.config.noValueText) : "<no value>";
            } else if (typeof valueInfo[name].value === "number" && valueSettings[name]) {
                handleInfo.values[name].ref.value.textContent = formatNumber(valueInfo[name].value, {
                    "unit": unitText,
                    "precision": valueSettings[name].digits,
                    "show-unit": false
                });
            } else {
                handleInfo.values[name].ref.value.textContent = valueInfo[name].value;
            }

            handleInfo.values[name].ref.unit.textContent = unitText === null ? "" : shmi.localize(unitText);
        });

        Object.keys(handleInfo.values).forEach((name) => {
            if (!valueInfo[name]) {
                handleInfo.values[name].ref.base.parentNode.removeChild(handleInfo.values[name].ref.base);
                delete handleInfo.values[name];
            }
        });
    }

    function checkHandleFocus(self, primary, activeSet) {
        const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary,
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            otherHandle = primary ? self.vars.handleInfo.secondary : self.vars.handleInfo.primary,
            otherRef = primary ? self.vars.elements.handle.secondary : self.vars.elements.handle.primary;

        if (activeSet) {
            handleInfo.recent = true;
            otherHandle.recent = false;
        }

        if (handleInfo.recent && otherHandle.active) {
            const parent = handleRef.base.parentNode,
                handleRect = handleRef.base.getBoundingClientRect(),
                otherRect = otherRef.base.getBoundingClientRect();
            if (!(otherRect.left >= handleRect.right ||
                otherRect.right <= handleRect.left ||
                otherRect.top >= handleRect.bottom ||
                otherRect.bottom <= handleRect.top)) {
                parent.insertBefore(otherRef.base, handleRef.base);
                handleRef.base.classList.add("focused");
                otherRef.base.classList.remove("focused");
            } else {
                handleRef.base.classList.remove("focused");
                otherRef.base.classList.remove("focused");
            }
        } else if (!otherHandle.active) {
            handleRef.base.classList.remove("focused");
            otherRef.base.classList.remove("focused");
        }
    }

    /**
     * set position of trend-handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     * @param {number} pos x-coordinate of handle
     */
    function setHandlePosition(self, primary, pos, activeSet = true) {
        if (!self.vars.datasets.length) {
            return;
        }
        const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary,
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            dims = self.vars.datasets[0].getDimensions(),
            { width } = dims,
            baseWidth = handleRef.base.offsetWidth;

        let offset = pos;
        if (offset < 1) {
            offset = 1;
        } else if (offset > width + 1) {
            offset = width + 1;
        }

        let reversed = false;
        if (offset > width + 1 - baseWidth) {
            handleRef.base.classList.add("reversed");
            reversed = true;
        } else {
            handleRef.base.classList.remove("reversed");
        }

        let styleOffset = reversed ? offset - baseWidth : offset;

        handleInfo.offset = offset;
        handleInfo.graphWidth = dims.width;
        handleRef.base.style.left = `${styleOffset}px`;
        updateHandleTime(self, primary);
        updateHandleValues(self, primary);

        if (!handleInfo.active) {
            handleInfo.active = true;
            handleRef.base.classList.add("active");
        }

        checkHandleFocus(self, primary, activeSet);

        clearTimeout(self.vars.handleUpdateTimeout);
        self.vars.handleUpdateTimeout = setTimeout(() => {
            if (self.isActive()) {
                self.fire("handle-info", self.getHandleInfo());
            }
        }, 100);
    }

    /**
     * get values to display at trend-handle
     *
     * @param {object} self instance reference
     * @param {number} posX x-coordinate of values
     * @return {object} display values
     */
    function getHandleValues(self, posX) {
        const values = {};

        self.vars.datasets.forEach((ds) => {
            const timestamp = ds.getTimeAt(posX),
                valueInfo = ds.getValue(timestamp);
            values[ds.name] = valueInfo;
        });

        return values;
    }

    /**
     * create input handler for graphs area
     *
     * @param {object} self instance reference
     * @returns {object} input handler
     */
    function getInputHandler(self) {
        let isScaling = false,
            scaleXFactor = 1,
            scaleYFactor = 1;
        return {
            onPress: (x, y, event) => {
                if (event.touches && event.touches.length > 1) {
                    isScaling = true;
                }
            },
            onRelease: (x, y, event) => {
                if (isScaling && event.touches && event.touches.length === 0) {
                    shmi.decouple(() => {
                        isScaling = false;
                        if (scaleXFactor === 1 && scaleYFactor === 1) {
                            self.setLive(!self.vars.live);
                        }
                        scaleXFactor = 1;
                        scaleYFactor = 1;
                    });
                }
            },
            onClick: (x, y, event) => {
                if (isScaling) {
                    return;
                }

                if (event.shiftKey) {
                    self.setLive(!self.vars.live);
                }

                const relPos = getRelativePosition(self.vars.elements.graphs, x, y);
                if (!event.shiftKey && self.config.handle.enabled) {
                    const handleInfo = self.vars.handleInfo;
                    if (!handleInfo.primary.active) {
                        setHandlePosition(self, true, relPos.x);
                    } else if (self.config.handle.secondary && !handleInfo.secondary.active) {
                        setHandlePosition(self, false, relPos.x);
                    } else if (!self.config.handle.secondary) {
                        setHandlePosition(self, true, relPos.x);
                    } else { //both active
                        const distPrimary = Math.abs(relPos.x - handleInfo.primary.offset),
                            distSecondary = Math.abs(relPos.x - handleInfo.secondary.offset);
                        if (distPrimary < distSecondary) {
                            setHandlePosition(self, true, relPos.x);
                        } else {
                            setHandlePosition(self, false, relPos.x);
                        }
                    }
                }
            },
            onDrag: (dx, dy, event) => {
                event.preventDefault();
                event.stopPropagation();
                if (dx === 0 && self.vars.zoomStage === 0) {
                    return;
                }
                updateDrag(self, dx, dy);
            },
            onWheel: (delta, event) => {
                event.preventDefault();
                event.stopPropagation();
                const relPos = getRelativePosition(self.vars.elements.graphs, event.clientX, event.clientY);
                if (!event.shiftKey) {
                    //Y axis zoom
                    if (delta > 0 && self.vars.zoomStage < self.vars.zoomStages.length - 1) { //zoom in (-> smaller value range)
                        self.vars.zoomStage = self.vars.zoomStage + 1;
                        self.vars.focusY = relPos.y;
                        updateZoomStage(self, relPos.y);
                    } else if (delta < 0 && self.vars.zoomStage > 0) { //zoom out (-> bigger value range)
                        self.vars.zoomStage = self.vars.zoomStage - 1;
                        self.vars.focusY = relPos.y;
                        updateZoomStage(self, relPos.y);
                    }
                } else {
                    //X axis zoom
                    const displayDuration = self.vars.displayDurationUpdate === null ? self.vars.displayDuration : self.vars.displayDurationUpdate;
                    let newDuration = displayDuration;

                    if (delta < 0 && displayDuration < self.vars.displayDurations[self.vars.displayDurations.length - 1]) { //zoom out (-> bigger duration)
                        newDuration = self.vars.displayDurations.find((duration) => duration > displayDuration);
                    } else if (delta > 0 && displayDuration > self.vars.displayDurations[0]) { //zoom in (-> smaller duration)
                        newDuration = self.vars.displayDurations.slice().reverse().find((duration) => duration < displayDuration);
                    }
                    if (newDuration !== displayDuration) {
                        if (self.vars.live) {
                            self.setLive(false);
                        }
                        clearTimeout(self.vars.durationUpdateTimeout);
                        self.vars.displayDurationUpdate = newDuration;
                        self.vars.durationUpdateTimeout = setTimeout(() => {
                            self.vars.displayDurationUpdate = null;
                            self.setDisplayDuration(newDuration, relPos.x);
                        }, 100);
                    }
                }
            },
            onScale: (scale, posX, posY, scaleX, scaleY, points) => {
                if ((scaleY >= 1.25 * scaleYFactor) || (scaleY <= 0.75 * scaleYFactor)) {
                    //zoom-y
                    if (scaleY > scaleYFactor && self.vars.zoomStage < self.vars.zoomStages.length - 1) { //zoom in (-> smaller value range)
                        self.vars.zoomStage = self.vars.zoomStage + 1;
                        self.vars.focusY = posY;
                        updateZoomStage(self, posY);
                        scaleYFactor *= 1.25;
                    } else if (scaleY < scaleYFactor && self.vars.zoomStage > 0) { //zoom out (-> bigger value range)
                        self.vars.zoomStage = self.vars.zoomStage - 1;
                        self.vars.focusY = posY;
                        updateZoomStage(self, posY);
                        scaleYFactor *= 0.75;
                    }
                }

                if ((scaleX >= 1.25 * scaleXFactor) || (scaleX <= 0.75 * scaleXFactor)) {
                    //zoom-x
                    const displayDuration = self.vars.displayDurationUpdate === null ? self.vars.displayDuration : self.vars.displayDurationUpdate;
                    let newDuration = displayDuration;

                    if (scaleX < scaleXFactor && displayDuration < self.vars.displayDurations[self.vars.displayDurations.length - 1]) { //zoom out (-> bigger duration)
                        newDuration = self.vars.displayDurations.find((duration) => duration > displayDuration);
                        scaleXFactor *= 0.75;
                    } else if (scaleX > scaleXFactor && displayDuration > self.vars.displayDurations[0]) { //zoom in (-> smaller duration)
                        newDuration = self.vars.displayDurations.slice().reverse().find((duration) => duration < displayDuration);
                        scaleXFactor *= 1.25;
                    }
                    if (newDuration !== displayDuration) {
                        if (self.vars.live) {
                            self.setLive(false);
                        }
                        clearTimeout(self.vars.durationUpdateTimeout);
                        self.vars.displayDurationUpdate = newDuration;
                        self.vars.durationUpdateTimeout = setTimeout(() => {
                            self.vars.displayDurationUpdate = null;
                            self.setDisplayDuration(newDuration, posX);
                        }, 100);
                    }
                }
            }
        };
    }

    /**
     * get position relative to specified element
     *
     * @param {HTMLElement} element base element
     * @param {number} clientX client x-coordinate
     * @param {number} clientY client y-coordinate
     * @return {object}  relative position
     */
    function getRelativePosition(element, clientX, clientY) {
        const bounds = element.getBoundingClientRect();
        return {
            x: clientX - bounds.left,
            y: clientY - bounds.top,
            bounds: bounds
        };
    }

    /**
     * initialize trend-handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     */
    function initHandle(self, primary) {
        const parent = self.vars.elements.graphs,
            base = self.vars.handleTemplate.cloneNode(true);

        const ref = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary;
        ref.base = base;
        ref.date = shmi.getUiElement("handle-date", base);
        ref.time = shmi.getUiElement("handle-time", base);
        ref.button = shmi.getUiElement("button", base);
        ref.values = shmi.getUiElement("data-values", base);
        ref.handle = shmi.getUiElement("drag-handle", base);
        ref.comment = shmi.getUiElement("comment-anchor", base);

        if (self.config.showComments) {
            const commentButton = shmi.createControl("iq-button", ref.comment, shmi.cloneObject(self.config.commentButtonConfig), "DIV");
            self.vars.eventTokens.push(commentButton.listen("click", async (evt) => {
                const trendInfo = self.getTrendInfo();
                const threads = shmi.requires("visuals.tools.threads");
                const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;
                if (trendInfo) {
                    const threadId = await threads.create("trend", trendInfo.trend_id, handleInfo.timestamp, {
                        dialogTitle: "${iqtrend_create_thread}",
                        titleLabel: "${iqtrend_threads_title}",
                        messageLabel: "${iqtrend_threads_message}",
                        applyLabel: "${iqtrend_threads_create_btn}",
                        enterMessageNotification: "${iqtrend_threads_enter_message_notification}"
                    });
                    if (threadId !== null) {
                        if (self.vars.datasource.source && typeof self.vars.datasource.source.reloadComments === "function") {
                            self.vars.datasource.source.reloadComments();
                        }
                    }
                }
            }));
        }

        const { MouseListener, TouchListener } = shmi.requires("visuals.io");
        //trend handle drag handler
        const inputHandlerHandle = {
            onPress: () => { /* implementation of handler stops event bubbling */ },
            onRelease: () => { /* implementation of handler stops event bubbling */ },
            onDrag: (dx, dy, event) => {
                const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;

                event.preventDefault();
                setHandlePosition(self, primary, handleInfo.offset + dx);
            }
        };
        const mhHandle = new MouseListener(ref.handle, inputHandlerHandle, { dragThreshold: 0 });
        const thHandle = new TouchListener(ref.handle, inputHandlerHandle, true, { dragThreshold: 0 });

        //prevent events on handle from propagating to graph area
        const inputHandlerBase = {
            onPress: () => { /* implementation of handler stops event bubbling */ },
            onRelease: () => { /* implementation of handler stops event bubbling */ },
            onClick: (x, y, event) => {
                checkHandleFocus(self, primary, true);
            },
            onWheel: (delta, event) => {
                event.stopPropagation();
            }
        };
        const mhBase = new MouseListener(ref.base, inputHandlerBase);
        const thBase = new TouchListener(ref.base, inputHandlerBase);

        //handle close button input
        const inputHandlerClose = {
            onPress: () => { /* implementation of handler stops event bubbling */ },
            onRelease: () => { /* implementation of handler stops event bubbling */ },
            onClick: () => {
                if (!self.isLocked()) {
                    ref.base.classList.remove("active");
                    const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;
                    handleInfo.active = false;
                    self.fire("handle-info", self.getHandleInfo());
                }
            }
        };
        const mhClose = new MouseListener(ref.button, inputHandlerClose);
        const thClose = new TouchListener(ref.button, inputHandlerClose);

        self.vars.inputListeners.push(mhHandle, thHandle, mhBase, thBase, mhClose, thClose);
        parent.appendChild(ref.base);
    }

    /**
     * update widget settings stored in local-storage
     *
     * @param {object} self control instance reference
     */
    function updateWidgetSettings(self) {
        if (self.config.rememberSelectedValues) {
            const handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self),
                username = shmi.requires("visuals.session.UserManager").currentUser.name,
                settingsText = localStorage.getItem(storageIdentifier);
            let widgetSettings = {};

            if (settingsText) {
                try {
                    widgetSettings = JSON.parse(settingsText);
                } catch (exc) {
                    console.error(`[${uiType}] could not parse saved selection:`, exc);
                }
            }

            widgetSettings[username] = widgetSettings[username] || {};
            widgetSettings[username][uiType] = widgetSettings[username][uiType] || {};
            widgetSettings[username][uiType][handle] = widgetSettings[username][uiType][handle] || {};
            widgetSettings[username][uiType][handle].selectedValues = shmi.cloneObject(self.vars.selectedValues);

            localStorage.setItem(storageIdentifier, JSON.stringify(widgetSettings));
        }
    }

    /**
     * fire 'display-time' event with current x-axis dimensions
     */
    function fireDisplayTimeUpdate(self, dims) {
        if (!self.isLive()) {
            shmi.caf(self.vars.displayTimeRafId);
            self.vars.displayTimeRafId = shmi.raf(() => {
                self.updateHandles();
                self.fire("display-time", dims);
                self.vars.lastDisplayTimeUpdate = Date.now();
            });
        } else if (Date.now() - self.vars.lastDisplayTimeUpdate > 1000) {
            self.fire("display-time", dims);
            self.vars.lastDisplayTimeUpdate = Date.now();
        }
    }

    /**
     * change option of scalegroup scale
     *
     * @param {object} self instance reference
     * @param {object} scaleGroup scalegroup reference
     * @param {string} option option name
     * @param {any} value option value
    */
    function setScaleGroupOption(self, scaleGroup, option, value) {
        const valueNames = Object.keys(scaleGroup.values);
        valueNames.forEach((valueName) => {
            const valueConfig = self.vars.selectedValues.find((vc) => vc.name === valueName);
            if (valueConfig) {
                valueConfig.options.scale = valueConfig.options.scale || {};
                valueConfig.options.scale[option] = value;
            }
        });

        if (option === "hidden") {
            scaleGroup.master.dataset.scale.setOptions({
                [option]: value
            });
        } else {
            valueNames.forEach((valueName) => {
                scaleGroup.values[valueName].dataset.scale.setOptions({
                    [option]: value
                });
            });
        }
    }

    // Definition of new control extending BaseControl - START
    /** control definition @type {object} */
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            datasets: [],
            xAxis: null,
            tokens: [],
            valueTokens: [],
            handleToken: null,
            rafId: 0,
            displayDuration: 60000,
            displayDurationUpdate: null,
            values: {},
            collectValues: false,
            inputListeners: [],
            zoomStage: 0,
            focusY: 50,
            elements: {
                scalesLeft: null,
                scalesRight: null,
                graphs: null,
                timeline: null,
                loadingIndicator: null,
                markerLayer: null,
                handle: {
                    primary: {
                        base: null,
                        time: null,
                        date: null,
                        button: null,
                        values: null,
                        handle: null
                    },
                    secondary: {
                        base: null,
                        time: null,
                        date: null,
                        button: null,
                        values: null,
                        handle: null
                    }
                }
            },
            handleInfo: {
                primary: {
                    active: false,
                    offset: 0,
                    timestamp: null,
                    graphWidth: null,
                    values: {},
                    recent: false
                },
                secondary: {
                    active: false,
                    offset: 0,
                    timestamp: null,
                    graphWidth: null,
                    values: {},
                    recent: false
                }
            },
            handleTemplate: null,
            handleValuesTemplate: null,
            markerTemplates: {
                comment: null,
                alarm: null
            },
            markerLayer: null,
            valueSettings: {},
            live: false,
            xAxisMode: "timeline",
            datasource: {
                type: "trend",
                name: "Trend",
                source: null
            },
            selectedValues: [],
            durationUpdateTimeout: 0,
            handleUpdateTimeout: 0,
            scaleGroups: {},
            loadingToken: null,
            markerToken: null,
            eventTokens: [],
            lastValueInfo: [],
            zoomStages: zoomStages,
            displayDurations: displayDurations,
            displayTimeRafId: 0,
            lastDisplayTimeUpdate: 0,
            restoreLiveToken: null
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: ["display-duration", "handle-info", "value-info", "graph-options", "live", "display-time"],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;
                self.element.style.overflow = "hidden"; //prevents observer errors in case stylesheet has not been loaded / overflow style is missing
                self.vars.elements.scalesLeft = shmi.getUiElement("scales-left", self.element);
                self.vars.elements.scalesRight = shmi.getUiElement("scales-right", self.element);
                self.vars.elements.graphs = shmi.getUiElement("graphs", self.element);
                self.vars.elements.xAxis = shmi.getUiElement("x-axis", self.element);
                self.vars.elements.loadingIndicator = shmi.getUiElement("loading-indicator", self.element);

                if (self.config.rememberSelectedValues) {
                    const settingsText = localStorage.getItem(storageIdentifier);
                    if (settingsText) {
                        try {
                            const handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self),
                                username = shmi.requires("visuals.session.UserManager").currentUser.name,
                                widgetSettings = JSON.parse(settingsText);

                            if (widgetSettings && widgetSettings[username] && widgetSettings[username][uiType] && widgetSettings[username][uiType][handle] && Array.isArray(widgetSettings[username][uiType][handle].selectedValues)) {
                                self.config.selectedValues = widgetSettings[username][uiType][handle].selectedValues;
                            }
                        } catch (exc) {
                            console.error(`[${uiType}] could not parse saved selection:`, exc);
                        }
                    }
                }

                self.vars.elements.markerLayer = shmi.getUiElement("marker-layer", self.element);
                if (!self.vars.elements.markerLayer) {
                    console.error(`[${uiType}] could not find required 'marker-layer' element in template.`);
                    return;
                }
                self.vars.markerTemplates.comment = shmi.getUiElement("trend-marker-comment", self.element);
                if (!self.vars.markerTemplates.comment) {
                    console.error(`[${uiType}] could not find required 'trend-marker-comment' element in template.`);
                    return;
                }
                const commentMarkerCopy = self.vars.markerTemplates.comment.cloneNode(true);
                self.vars.markerTemplates.comment.parentNode.removeChild(self.vars.markerTemplates.comment);
                self.vars.markerTemplates.comment = commentMarkerCopy;

                self.vars.markerTemplates.alarm = shmi.getUiElement("trend-marker-alarm", self.element);
                if (!self.vars.markerTemplates.alarm) {
                    console.error(`[${uiType}] could not find required 'trend-marker-alarm' element in template.`);
                    return;
                }
                const alarmMarkerCopy = self.vars.markerTemplates.alarm.cloneNode(true);
                self.vars.markerTemplates.alarm.parentNode.removeChild(self.vars.markerTemplates.alarm);
                self.vars.markerTemplates.alarm = alarmMarkerCopy;

                self.vars.handleTemplate = shmi.getUiElement("trend-handle", self.element);
                if (!self.vars.handleTemplate) {
                    console.error(`[${uiType}] could not find required 'trend-handle' element in template.`);
                    return;
                }
                const handleTemplateCopy = self.vars.handleTemplate.cloneNode(true);
                self.vars.handleTemplate.parentNode.removeChild(self.vars.handleTemplate);
                self.vars.handleTemplate = handleTemplateCopy;

                self.vars.handleValuesTemplate = shmi.getUiElement("data-box", self.vars.handleTemplate);
                if (!self.vars.handleValuesTemplate) {
                    console.error(`[${uiType}] could not find required 'data-box' element in template.`);
                    return;
                }
                const valuesTemplateCopy = self.vars.handleValuesTemplate.cloneNode(true);
                self.vars.handleValuesTemplate.parentNode.removeChild(self.vars.handleValuesTemplate);
                self.vars.handleValuesTemplate = valuesTemplateCopy;

                if (Array.isArray(self.config.availableDurations)) {
                    self.vars.displayDurations = self.config.availableDurations;
                    if (!self.vars.displayDurations.length) {
                        console.warn(`[IqTrend] no display durations configured, adding 60000ms`);
                        self.vars.displayDurations.push(60000);
                    }
                    self.vars.displayDurations.sort((a, b) => a - b);
                }

                if (Array.isArray(self.config.availableZoomStages)) {
                    self.vars.zoomStages = self.config.availableZoomStages.filter((zoomStage) => {
                        if (zoomStage < 100) {
                            console.warn(`[IqTrend] configured zoom-stage is below 100 and will not be available: ${zoomStage}`);
                            return false;
                        }
                        return true;
                    });
                    if (!self.vars.zoomStages.includes(100)) {
                        console.warn(`[IqTrend] configured zoom-stages do not include 100% - original zoom-stage added`);
                        self.vars.zoomStages.push(100);
                    }
                    self.vars.zoomStages.sort((a, b) => a - b);
                }

                initHandle(self, true);
                initHandle(self, false);

                const { MouseListener, TouchListener } = shmi.requires("visuals.io");
                self.vars.displayDuration = self.config.displayDuration;
                const inputHandler = getInputHandler(this);
                this.vars.inputListeners.push(
                    new MouseListener(self.vars.elements.graphs, inputHandler, { dragThreshold: 10 })
                );

                this.vars.inputListeners.push(
                    new TouchListener(self.vars.elements.graphs, inputHandler, true, { dragThreshold: 10 })
                );

                //prevent context menu from opening when pausing / unpausing live trend
                self.vars.elements.graphs.oncontextmenu = (e) => {
                    e.preventDefault();
                };

                if (self.vars.xAxisMode === "timeline") {
                    const { Timeline } = shmi.requires("visuals.controls.iqtrend.timeline");
                    const now = Date.now();
                    this.vars.xAxis = new Timeline(self.vars.elements.xAxis, {
                        min: now - self.vars.displayDuration,
                        max: now,
                        numSteps: 21,
                        mirrored: false,
                        markerFormats: self.config.timelineMarkerFormats,
                        startMarkerFormat: self.config.timelineStartMarkerFormat
                    }, fireDisplayTimeUpdate.bind(null, self));
                } else {
                    const { XAxis } = shmi.requires("visuals.controls.iqtrend.xaxis");
                    this.vars.xAxis = new XAxis(self.vars.elements.xAxis, {
                        min: 0,
                        max: 100,
                        numSteps: 21,
                        mirrored: false
                    }, fireDisplayTimeUpdate.bind(null, self));
                }

                if (self.config.showComments) {
                    const { MarkerLayer } = shmi.requires("visuals.controls.iqtrend.markerlayer");
                    this.vars.markerLayer = new MarkerLayer(this.vars.elements.markerLayer, this);
                }

                if (this.config.datasource && typeof this.config.datasource.type === "string" && typeof this.config.datasource.name === "string") {
                    this.vars.datasource.type = this.config.datasource.type;
                    this.vars.datasource.name = this.config.datasource.name;
                }

                if (this.vars.datasource.type === "trend") {
                    const { DataSourceTrend } = shmi.requires("visuals.controls.iqtrend.datasourceTrend");
                    this.vars.datasource.source = new DataSourceTrend(this.vars.datasource.name);
                    this.vars.loadingToken = this.vars.datasource.source.watch(({
                        loading
                    }) => {
                        if (typeof loading === "boolean") {
                            if (loading) {
                                shmi.addClass(self.vars.elements.loadingIndicator, "loading");
                            } else {
                                shmi.removeClass(self.vars.elements.loadingIndicator, "loading");
                            }
                        }
                    });
                }
            },
            /**
             * add graph value
             *
             * @param {string} itemName name of value
             * @param {object} options value options
            */
            addGraphValue: function(itemName, options) {
                const self = this;
                const valueConfig = shmi.cloneObject(options);

                self.vars.selectedValues.push({
                    name: itemName,
                    options: valueConfig
                });
                self.vars.valueTokens.push(addGraphValue(self, itemName, valueConfig));

                updateHighlightState(self);
                self.vars.datasource.source.select(itemName);
                updateWidgetSettings(self);
            },
            /**
             * remove graph value
             *
             * @param {string|number} itemNameOrIndex name of value or index to remove
             */
            removeGraph: function(itemNameOrIndex) {
                const self = this;
                let idx = -1,
                    name = null;

                if (typeof itemNameOrIndex === "number") {
                    if (self.vars.selectedValues[itemNameOrIndex]) {
                        idx = itemNameOrIndex;
                    }
                } else if (typeof itemNameOrIndex === "string") {
                    idx = self.vars.selectedValues.findIndex((valueConfig) => valueConfig.name === itemNameOrIndex);
                }

                if (idx >= 0) {
                    if (self.vars.selectedValues.length === 1 && self.isLive()) {
                        self.setLive(false);
                    }
                    name = self.vars.selectedValues[idx].name;
                    if (self.vars.selectedValues[idx].options && self.vars.selectedValues[idx].options.highlight) {
                        self.setGraphOptions(name, { highlight: false });
                    }
                    self.vars.selectedValues.splice(idx, 1);
                    self.vars.datasource.source.deselect(name);
                    self.vars.valueTokens[idx].setToken.unwatch();
                    self.vars.valueTokens.splice(idx, 1);

                    self.vars.datasets[idx].remove();
                    self.vars.datasets.splice(idx, 1);

                    updateWidgetSettings(self);
                    self.updateHandles();
                }
            },
            /**
             * set graph options at runtime
             *
             * @param {string} name name of value
             * @param {object} options graph options
             * @param {number} [options.style] value style index
             * @param {boolean} [options.drawTolerance] enable or disable display of tolerances (if available)
             * @param {boolean} [options.fillCurve] fill area below curve if enabled
             * @param {boolean} [options.highlight] highlight curve if enabled
             * @param {object} [options.limits] scale limit settings
             * @param {string} [options.limits.mode] either `"config"`, `"auto"` or `"manual"`
             * @param {number} [options.limits.step] scale limit stepping when mode is `"auto"`
             * @param {number} [options.limits.min] minimum scale value when mode is `"manual"`
             * @param {number} [options.limits.max] maximum scale value when mode is `"manual"`
             * @param {number|null} [options.scaleGroup] scale group id, `null` to remove
             * @param {object} [options.scale] scale appearance settings
             * @param {string} [options.scale.location] scale location, either `"left"` or `"right"`
             * @param {boolean} [options.scale.hidden] hide scale if enabled
             */
            setGraphOptions(name, options) {
                const self = this,
                    dataset = self.vars.datasets.find((ds) => ds.name === name),
                    selectedValue = self.vars.selectedValues.find((value) => value.name === name),
                    datasetOptions = {},
                    datasourceOptions = {};
                let updateDataset = false,
                    updateHandles = false,
                    updateDatasource = false;

                if (!(dataset && selectedValue)) {
                    console.warn(`[IqTrend] cannot set options for unknown graph: ${name}`);
                    return;
                }
                if (typeof options.style === "number") {
                    const styles = self.getValueStyles();
                    datasetOptions.style = styles[options.style] ? styles[options.style] : self.config.defaultValueStyle;
                    selectedValue.options.style = options.style;
                    updateDataset = true;
                    updateHandles = true;
                }

                if (typeof options.drawTolerance === "boolean") {
                    datasetOptions.drawTolerance = options.drawTolerance;
                    selectedValue.options.drawTolerance = options.drawTolerance;
                    datasourceOptions.tolerance = {
                        name,
                        drawTolerance: options.drawTolerance
                    };
                    updateDataset = true;
                    updateDatasource = true;
                }

                if (typeof options.fillCurve === "boolean") {
                    datasetOptions.fillCurve = options.fillCurve;
                    selectedValue.options.fillCurve = options.fillCurve;
                    updateDataset = true;
                }

                if (options.limits && typeof options.limits.mode === "string") {
                    selectedValue.options.limits = selectedValue.options.limits || {};
                    selectedValue.options.limits = options.limits;
                    const limitsOpt = shmi.cloneObject(options.limits);
                    limitsOpt.name = name;
                    datasourceOptions.limits = limitsOpt;
                    updateDatasource = true;
                }

                if (options.scaleGroup === null || typeof options.scaleGroup === "number") {
                    if (dataset.groupId !== options.scaleGroup) {
                        selectedValue.options.scaleGroup = options.scaleGroup;
                        if (dataset.scaleGroup && typeof dataset.groupId === "number") {
                            dataset.scaleGroup.removeValue(name);
                        }
                        dataset.groupId = options.scaleGroup;

                        if (options.scaleGroup === null) {
                            if (dataset.scaleGroup && dataset.scaleGroup.getValueNames().length === 0) {
                                delete self.vars.scaleGroups[dataset.scaleGroup.groupId];
                            }
                            dataset.scaleGroup = null;
                        } else if (!self.vars.scaleGroups[options.scaleGroup]) {
                            const { ScaleGroup } = shmi.requires("visuals.controls.iqtrend.scalegroup");
                            self.vars.scaleGroups[options.scaleGroup] = dataset.scaleGroup = new ScaleGroup(options.scaleGroup, this, dataset);
                        } else {
                            dataset.scaleGroup = self.vars.scaleGroups[options.scaleGroup];
                            dataset.scaleGroup.addValue(name, dataset);
                        }
                    }
                }

                if (options.scale) {
                    if (typeof options.scale.location === "string") {
                        if (dataset.scaleGroup) {
                            setScaleGroupOption(self, dataset.scaleGroup, "location", options.scale.location);
                        } else {
                            selectedValue.options.scale = selectedValue.options.scale || {};
                            selectedValue.options.scale.location = options.scale.location;
                            dataset.scale.setOptions({
                                location: options.scale.location
                            });
                        }
                    }

                    if (typeof options.scale.hidden === "boolean") {
                        if (dataset.scaleGroup) {
                            setScaleGroupOption(self, dataset.scaleGroup, "hidden", options.scale.hidden);
                        } else {
                            selectedValue.options.scale = selectedValue.options.scale || {};
                            selectedValue.options.scale.hidden = options.scale.hidden;
                            if (options.scale.hidden !== dataset.scale.hidden) {
                                dataset.scale.setOptions({
                                    hidden: options.scale.hidden
                                });
                            }
                        }
                    }
                }

                if (typeof options.highlight === "boolean") {
                    self.vars.datasets.forEach((ds, idx) => {
                        if (options.highlight === false || ds.name !== name) {
                            self.vars.selectedValues[idx].options.highlight = false;
                        } else {
                            self.vars.selectedValues[idx].options.highlight = true;
                        }
                    });
                    updateHighlightState(self);
                    updateHandles = true;
                }

                if (updateDataset) {
                    dataset.setOptions(datasetOptions);
                }

                if (updateDatasource) {
                    self.vars.datasource.source.setOptions(datasourceOptions);
                }

                if (updateHandles) {
                    self.updateHandles();
                }

                self.fire("graph-options", shmi.cloneObject(selectedValue));
                updateWidgetSettings(self);
            },
            /**
             * get currently selected values
             *
             * @returns {TrendValueConfig[]} selected value info
             */
            getSelectedValues: function() {
                const self = this,
                    selected = shmi.cloneObject(self.vars.selectedValues);

                selected.forEach((selection) => {
                    const dataset = self.vars.datasets.find((ds) => ds.name === selection.name);
                    if (dataset && dataset.scaleGroup) {
                        selection.options.scale = selection.options.scale || {};
                        selection.options.scale.location = dataset.scaleGroup.master.dataset.scale.location;
                        selection.options.scale.hidden = dataset.scaleGroup.master.dataset.scale.hidden;
                    }
                });

                return selected;
            },
            /**
             * set selected values
             *
             * @param {TrendValueConfig[]} selectedValues selected value info
             */
            setSelectedValues: function(selectedValues) {
                if (!this.isActive()) {
                    console.error("Cannot set selected values while widget is inactive.");
                    return;
                }
                if (!Array.isArray(selectedValues) || !selectedValues.every((value) => typeof value.name === "string" && value.options && typeof value.options === "object")) {
                    console.error("Parameter contains invalid value definitions:", selectedValues);
                    throw new Error("Parameter contains invalid value definitions.");
                }
                const self = this,
                    selectedNames = self.vars.selectedValues.map((selected) => selected.name),
                    wasLive = self.isLive() || (selectedNames.length === 0 && self.config.startLive);

                if (self.vars.restoreLiveToken) {
                    self.vars.restoreLiveToken.unwatch();
                    self.vars.restoreLiveToken = null;
                }

                self.vars.selectedValues = shmi.cloneObject(selectedValues);
                self.vars.datasource.source.deselect(...selectedNames);
                self.vars.valueTokens.forEach((t) => {
                    t.setToken.unwatch();
                });
                self.vars.valueTokens = [];

                self.vars.datasets.forEach((d) => {
                    d.remove();
                });
                self.vars.datasets = [];

                self.vars.selectedValues.forEach((selected) => {
                    self.vars.valueTokens.push(addGraphValue(self, selected.name, selected.options));
                });
                self.vars.datasource.source.select(...self.vars.selectedValues.map((selected) => selected.name));

                if (wasLive) {
                    if (!self.vars.datasource.source._busy) {
                        self.setLive(true);
                    } else {
                        self.vars.restoreLiveToken = self.vars.datasource.source.watch(() => {
                            if (!self.vars.datasource.source._busy) {
                                self.vars.restoreLiveToken.unwatch();
                                self.vars.restoreLiveToken = null;
                                self.setLive(true);
                            }
                        });
                    }
                }

                updateHighlightState(self);
                updateWidgetSettings(self);
            },
            /**
             * set displayed duration
             *
             * @param {number} displayDuration displayed duration
             * @param {number} [focusX] optional focus coordinate for duration change
             */
            setDisplayDuration: function(displayDuration, focusX = 0) {
                if (this.vars.live) {
                    this.setLive(false);
                }
                this.vars.displayDuration = displayDuration;
                this.fire("display-duration", { duration: this.vars.displayDuration });
                updateDisplayDuration(this, displayDuration, focusX);
            },
            /**
             * get currently displayed duration
             * @returns {number} displayed duration in ms
             */
            getDisplayDuration: function() {
                return this.vars.displayDuration;
            },
            /**
             * get configured display durations
             *
             * @returns {number[]} configured display durations
             */
            getDisplayDurations: function() {
                return shmi.cloneObject(this.vars.displayDurations);
            },
            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.live = (self.config.startLive === true);

                if (Array.isArray(self.config.selectedValues)) {
                    self.vars.selectedValues = shmi.cloneObject(self.config.selectedValues);
                }

                self.vars.selectedValues.forEach((selected) => {
                    self.vars.valueTokens.push(addGraphValue(self, selected.name, selected.options));
                });
                updateHighlightState(self);

                self.vars.handleToken = self.vars.datasource.source.watch(({
                    info,
                    data
                }) => {
                    if (info && data) {
                        if (info.values) {
                            self.vars.datasets.forEach((dataset) => {
                                const itemName = dataset.name;
                                if (info.values[itemName]) {
                                    dataset.setOptions({
                                        interval: info.values[itemName].interval,
                                        unit: info.values[itemName].unit,
                                        discrete: info.values[itemName].data_type === "boolean" ? true : info.values[itemName].discrete
                                    });
                                    if (info.values[itemName].data_type === "string" && !dataset.scale.hidden) {
                                        dataset.scale.setOptions({ hidden: true });
                                    }
                                    updateZoomStage(self, self.vars.focusY, dataset);
                                }
                                if (data.values[itemName]) {
                                    dataset.clearValues();
                                    dataset.addValues(getSourceDataValues(data, itemName));
                                    dataset.update();
                                }
                            });
                            self.updateHandles();

                            const values = shmi.cloneObject(info.values),
                                valueNames = [];
                            Object.keys(values).forEach((name) => {
                                if (typeof values[name].interval === "number" && typeof values[name].data_type === "string") {
                                    valueNames.push(name);
                                } else {
                                    delete values[name];
                                }
                            });
                            valueNames.sort();
                            if (valueNames.length !== self.vars.lastValueInfo.length || !self.vars.lastValueInfo.every((valueName, idx) => valueName === valueNames[idx])) {
                                self.fire("value-info", shmi.cloneObject(values));
                                self.vars.lastValueInfo = valueNames;
                            }
                        }
                    }
                });

                const selectedNames = self.vars.selectedValues.map((selected) => selected.name);
                self.vars.datasource.source.select(...selectedNames);

                self.vars.inputListeners.forEach((l) => {
                    l.enable();
                });

                if (self.config.showComments) {
                    self.vars.markerToken = self.vars.datasource.source.watch(({
                        comments,
                        alarms
                    }) => {
                        if (comments) {
                            self.vars.markerLayer.clearMarkers("comment");
                            self.vars.markerLayer.addMarkers(comments);
                        }

                        if (alarms) {
                            self.vars.markerLayer.clearMarkers("alarm");
                            self.vars.markerLayer.addMarkers(alarms);
                        }
                    });
                }
                self.controls.forEach((c) => {
                    c.enable();
                });

                self.setLive(self.vars.live);
            },
            /**
             * check if live mode is active
             *
             * @returns {boolean} `true` if live mode is active, `false` else
            */
            isLive: function() {
                return this.vars.live;
            },
            /**
             * set state of live mode
             * @param {boolean} isLive `true` to activate live mode, `false` to deativate
             */
            setLive: function(isLive) {
                const self = this,
                    { source } = self.vars.datasource;
                if (!source.hasLive) {
                    console.log(`[${uiType}] live mode not available with current datatsource.`);
                    return;
                }

                if (isLive === source.isLive) {
                    return;
                }
                self.vars.live = source.setLive(isLive);
                self.fire("live", {
                    isLive: self.vars.live
                });

                function updateGraph() {
                    if (self.isActive() && self.vars.live) {
                        const newMax = Date.now(),
                            newMin = newMax - self.vars.displayDuration;

                        self.vars.xAxis.setOptions({
                            x: {
                                min: newMin,
                                max: newMax
                            }
                        });
                        self.vars.datasets.forEach((g) => {
                            g.setOptions({
                                x: {
                                    min: newMin,
                                    max: newMax
                                }
                            },
                            true);
                        });
                        if (self.vars.markerLayer) {
                            self.vars.markerLayer.setOptions({
                                x: {
                                    min: newMin,
                                    max: newMax
                                }
                            });
                        }
                        self.vars.datasource.source.setOptions({
                            tStart: newMin
                        }, true);

                        self.updateHandles();

                        self.vars.rafId = shmi.raf(updateGraph);
                    }
                }

                if (self.vars.live) {
                    updateGraph();
                } else {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = 0;
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                shmi.caf(self.vars.rafId);
                self.vars.rafId = 0;

                if (self.vars.live) {
                    self.setLive(false);
                }

                if (self.vars.handleToken) {
                    self.vars.handleToken.unwatch();
                    self.vars.handleToken = null;
                }
                const selectedNames = self.vars.selectedValues.map((selected) => selected.name);
                self.vars.datasource.source.deselect(...selectedNames);
                self.vars.valueTokens.forEach((t) => {
                    t.setToken.unwatch();
                });
                self.vars.valueTokens = [];

                self.vars.datasets.forEach((d) => {
                    d.remove();
                });
                self.vars.datasets = [];

                self.vars.inputListeners.forEach((l) => {
                    l.disable();
                });

                self.controls.forEach((c) => {
                    c.disable();
                });
                self.vars.lastValueInfo = [];
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                self.vars.inputListeners.forEach((l) => {
                    l.disable();
                });

                self.controls.forEach((c) => {
                    c.lock();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.vars.inputListeners.forEach((l) => {
                    l.enable();
                });

                self.controls.forEach((c) => {
                    c.unlock();
                });
            },
            /* called when control is deleted - instance cleanup */
            onDelete: function() {
                if (this.vars.xAxis) {
                    this.vars.xAxis.remove();
                    this.vars.xAxis = null;
                }

                if (this.vars.loadingToken) {
                    this.vars.loadingToken.unwatch();
                    this.vars.loadingToken = null;
                }

                this.vars.eventTokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.eventTokens = [];
            },
            /**
             * set display options
             * @param {object} options display options
             */
            setOptions: function(options) {
                const self = this,
                    { xAxis } = self.vars;

                if (typeof options.tStart === "number") {
                    if (self.vars.live) {
                        self.setLive(false);
                    }
                    xAxis.setOptions({
                        x: {
                            min: options.tStart,
                            max: options.tStart + self.vars.displayDuration
                        }
                    });
                    self.vars.datasets.forEach((ds) => {
                        ds.setOptions({
                            x: {
                                min: options.tStart,
                                max: options.tStart + self.vars.displayDuration
                            }
                        },
                        true);
                    });
                    if (self.vars.markerLayer) {
                        self.vars.markerLayer.setOptions({
                            x: {
                                min: options.tStart,
                                max: options.tStart + self.vars.displayDuration
                            }
                        });
                    }
                    self.vars.datasource.source.setOptions({
                        tStart: options.tStart
                    });
                }
            },
            /**
             * update trend handles
             */
            updateHandles() {
                if (this.vars.handleInfo.primary.active) {
                    updateHandleTime(this, true);
                    updateHandleValues(this, true);
                }
                if (this.vars.handleInfo.secondary.active) {
                    updateHandleTime(this, false);
                    updateHandleValues(this, false);
                }
            },
            /**
             * get trend handle information
             * @returns {object} handle info
             */
            getHandleInfo() {
                const handleInfo = shmi.cloneObject(this.vars.handleInfo);
                if (handleInfo.primary.active) {
                    delete handleInfo.primary.active;
                    delete handleInfo.primary.graphWidth;
                    delete handleInfo.primary.offset;
                    Object.keys(handleInfo.primary.values).forEach((valueName) => {
                        handleInfo.primary.values[valueName] = handleInfo.primary.values[valueName].info;
                    });
                } else {
                    handleInfo.primary = null;
                }

                if (handleInfo.secondary.active) {
                    delete handleInfo.secondary.active;
                    delete handleInfo.secondary.graphWidth;
                    delete handleInfo.secondary.offset;
                    Object.keys(handleInfo.secondary.values).forEach((valueName) => {
                        handleInfo.secondary.values[valueName] = handleInfo.secondary.values[valueName].info;
                    });
                } else {
                    handleInfo.secondary = null;
                }

                return handleInfo;
            },
            /**
             * get information on displayed values
             * @returns {object} value info
             */
            getValueInfo() {
                if (this.vars.datasource.source) {
                    return this.vars.datasource.source.getValueSettings();
                }
                return null;
            },
            /**
             * get information on currently configured trend / datasource
             * @returns {object} trend info
             */
            getTrendInfo() {
                if (this.vars.datasource.source) {
                    const trendInfo = this.vars.datasource.source.getTrendSettings();
                    trendInfo.type = this.vars.datasource.type;
                    trendInfo.name = this.vars.datasource.name;
                    return trendInfo;
                }
                return null;
            },
            /**
             * get inactive graph style
             * @returns {object} inactive graph style
             */
            getInactiveStyle() {
                return shmi.cloneObject(this.config.inactiveStyle ? this.config.inactiveStyle : defaultInactiveStyle);
            },
            /**
             * get default graph style
             * @returns {object} default graph style
             */
            getDefaultStyle() {
                return shmi.cloneObject(this.config.defaultValueStyle ? this.config.defaultValueStyle : defaultValueStyle);
            },
            /**
             * get configured value styles
             *
             * @returns {object[]} value styles
             */
            getValueStyles() {
                return shmi.cloneObject(this.config.valueStyles);
            },
            setHandlePosition(primary = true, pos, activeSet = true) {
                setHandlePosition(this, primary, pos, activeSet);
            },
            /**
             * Zoom in Y-axis
             */
            zoomInY() {
                const self = this;

                if (self.vars.zoomStage < self.vars.zoomStages.length - 1) {
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posY = Math.floor(rect.height / 2);

                    self.vars.zoomStage = self.vars.zoomStage + 1;
                    self.vars.focusY = posY;
                    updateZoomStage(self, posY);
                }
            },
            /**
             * Zoom out Y-axis
             */
            zoomOutY() {
                const self = this;

                if (self.vars.zoomStage > 0) {
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posY = Math.floor(rect.height / 2);

                    self.vars.zoomStage = self.vars.zoomStage - 1;
                    self.vars.focusY = posY;
                    updateZoomStage(self, posY);
                }
            },
            /**
             * get current zoom stage
             * @returns {number} current zoom stage
            */
            getZoomStage() {
                return this.vars.zoomStages[this.vars.zoomStage];
            },
            /**
             * get configured zoom stages
             * @returns {number[]} configured zoom stages
             */
            getZoomStages() {
                return shmi.cloneObject(this.vars.zoomStages);
            },
            /**
             * Zoom in X-axis
             */
            zoomInX() {
                const self = this,
                    { displayDuration } = self.vars;
                let newDuration = displayDuration;

                if (displayDuration > self.vars.displayDurations[0]) { //zoom in (-> smaller duration)
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posX = Math.floor(rect.width / 2);
                    newDuration = self.vars.displayDurations.slice().reverse().find((duration) => duration < displayDuration);
                    if (self.vars.live) {
                        self.setLive(false);
                    }
                    self.setDisplayDuration(newDuration, posX);
                }
            },
            /**
             * Zoom out X-axis
             */
            zoomOutX() {
                const self = this,
                    { displayDuration } = self.vars;
                let newDuration = displayDuration;

                if (displayDuration < self.vars.displayDurations[self.vars.displayDurations.length - 1]) { //zoom out (-> bigger duration)
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posX = Math.floor(rect.width / 2);
                    newDuration = self.vars.displayDurations.find((duration) => duration > displayDuration);
                    if (self.vars.live) {
                        self.setLive(false);
                    }
                    self.setDisplayDuration(newDuration, posX);
                }
            },
            /**
             * get x-axis dimensions
             *
             * @returns {object} x-axis dimensions
             */
            getDimensionsX() {
                return this.vars.xAxis.getDimensions();
            }
        }
    };

    /**
     * @typedef {object} TrendValueConfig
     * @property {string} name name of trend value
     * @property {object} options value options
     * @property {boolean} [options.drawTolerance] enable or disable display of tolerances (if available)
     * @property {boolean} [options.fillCurve] fill area below curve if enabled
     * @property {boolean} [options.highlight] highlight curve if enabled
     * @property {object} [options.limits] scale limit settings
     * @property {string} [options.limits.mode] either `"config"`, `"auto"` or `"manual"`
     * @property {number} [options.limits.step] scale limit stepping when mode is `"auto"`
     * @property {number} [options.limits.min] minimum scale value when mode is `"manual"`
     * @property {number} [options.limits.max] maximum scale value when mode is `"manual"`
     * @property {number|null} [options.scaleGroup] scale group id, `null` to remove
     * @property {object} [options.scale] scale appearance settings
     * @property {string} [options.scale.location] scale location, either `"left"` or `"right"`
     * @property {boolean} [options.scale.hidden] hide scale if enabled
     */

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * Module to implement commonly used features for iq-trend
 *
 * @module visuals/controls/iqtrend/common
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.common",
        /** @lends module:visuals/controls/iqtrend/common */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    let canvas = null,
        context = null;

    /**
     * states of trend-tasks
     * @const
     */
    module.taskStates = {
        CREATED: 1,
        RUNNING: 2,
        DONE: 3,
        ERROR: 4,
        CANCELED: 5
    };

    /**
     * calculate dimensions of specified text
     *
     * @param {string} text input text
     * @param {string} fontStyle font style to use for caclulation
     * @returns {object} text dimension info
    */
    module.getTextDimensions = function(text, fontStyle = "400 12px RobotoLight") {
        if (!canvas) {
            canvas = document.createElement("canvas");
        }
        if (!context) {
            context = canvas.getContext("2d");
        }
        context.font = fontStyle;
        const dimensions = context.measureText(text);
        dimensions.fontHeight = dimensions.fontBoundingBoxAscent + dimensions.fontBoundingBoxDescent;
        dimensions.textHeight = dimensions.actualBoundingBoxAscent + dimensions.actualBoundingBoxDescent;
        return dimensions;
    };

    /**
     * get font style of specified element
     *
     * @param {HTMLElement} element element to retrieve font style from
     * @returns {string} font style
    */
    module.getFontStyle = function(element) {
        const style = getComputedStyle(element);
        return `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
    };
}());

/**
 * Module to implement dataset (graph + scale) structure for use in iq-trend
 *
 * @module visuals/controls/iqtrend/dataset
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.dataset",
        /** @lends module:visuals/controls/iqtrend/dataset */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * check if dataset if master dataset of configured scale group
     *
     * @param {object} self dataset reference
     * @return {boolean} `true` if dataset is scale group master, `false` else
     */
    function isGroupMaster(self) {
        return self.scaleGroup && self.scaleGroup.master.dataset === self;
    }

    /**
     * DataSet definition
     */
    module.DataSet = class DataSet {
        /**
         * Creates an instance of DataSet.
         *
         * @param {object} elements base elements reference
         * @param {HTMLElement} elements.graphs base element for graphs
         * @param {HTMLElement} elements.scalesLeft base element for left-side scales
         * @param {HTMLElement} elements.scalesRight base element for right-side scales
         * @param {object} options dataset options
         * @param {object} trendWidget iq-trend reference
         */
        constructor(elements, options, trendWidget) {
            const { Graph } = shmi.requires("visuals.controls.iqtrend.graph"),
                { Scale } = shmi.requires("visuals.controls.iqtrend.scale"),
                { ScaleGroup } = shmi.requires("visuals.controls.iqtrend.scalegroup");

            this.name = options.name;
            this.trendWidget = trendWidget;
            this.groupId = options.scaleGroup;
            options.dataset = this;

            const { scaleGroups } = this.trendWidget.vars;
            if (this.groupId !== null) {
                if (scaleGroups[this.groupId]) {
                    this.scaleGroup = scaleGroups[this.groupId];
                    this.scaleGroup.addValue(this.name, this);
                } else {
                    this.scaleGroup = scaleGroups[this.groupId] = new ScaleGroup(this.groupId, this.trendWidget, this);
                }
            }

            this.graph = new Graph(elements.graphs, options);

            if (options.scale && options.scale.location === "right") {
                this.scale = new Scale(elements.scalesRight, {
                    mirrored: options.scale.align !== "left",
                    align: options.scale.align ? options.scale.align : "right",
                    location: "right",
                    hidden: this.scaleGroup && !isGroupMaster(this) ? true : options.scale && typeof options.scale.hidden === "boolean" ? options.scale.hidden : false,
                    valueName: options.name,
                    autoWidth: !!this.trendWidget.config.autoScaleWidth
                }, this);
            } else {
                this.scale = new Scale(elements.scalesLeft, {
                    mirrored: options.scale && options.scale.align === "right",
                    align: options.scale && options.scale.align ? options.scale.align : "left",
                    location: "left",
                    hidden: this.scaleGroup && !isGroupMaster(this) ? true : options.scale && typeof options.scale.hidden === "boolean" ? options.scale.hidden : false,
                    valueName: options.name,
                    autoWidth: !!this.trendWidget.config.autoScaleWidth
                }, this);
            }

            this.trendWidget.vars.datasets.push(this);

            if (this.scaleGroup && !isGroupMaster(this)) {
                this.scaleGroup.master.dataset.scale.setOptions({
                    addValue: this.name
                });
            }
        }
        /**
         * retrieve graph dimentsions
         *
         * @return {object} graph dimensions
         */
        getDimensions() {
            return this.graph.getDimensions();
        }
        /**
         * set dataset options
         * @param {object} options dataset options
         */
        setOptions(options) {
            if (options.y && this.scaleGroup) {
                this.scaleGroup.setOptions({
                    y: options.y
                }, this.name);
                options = shmi.cloneObject(options);
                delete options.y;
            }
            this.graph.setOptions(options);
            this.scale.setOptions(options);
            if (options.style && this.scaleGroup && !isGroupMaster(this)) {
                this.scaleGroup.master.dataset.scale.setOptions({
                    style: options.style
                });
            }
        }
        /**
         * add value to graph
         */
        addValue() {
            this.graph.addValue.apply(this.graph, arguments);
        }
        /**
         * add array of values to graph
         */
        addValues() {
            this.graph.addValues.apply(this.graph, arguments);
        }
        /**
         * clear graph values
         */
        clearValues() {
            this.graph.clearValues();
        }
        /**
         * get graph value at specified x-coordinate
         */
        getValue() {
            return this.graph.getValue.apply(this.graph, arguments);
        }
        /**
         * get graph value at specified y-coordinate
         */
        getValueAtY() {
            return this.graph.getValueAtY.apply(this.graph, arguments);
        }
        /**
         * get timestamp at specified x-coordinate
         */
        getTimeAt() {
            return this.graph.getTimeAt.apply(this.graph, arguments);
        }
        /**
         * watch for changes of graph dimensions
         */
        watch() {
            return this.graph.watch.apply(this.graph, arguments);
        }
        /**
         * update graph & scale
         */
        update() {
            this.graph.draw();
            this.scale.draw();
        }
        /**
         * remove this dataset
         */
        remove() {
            this.graph.remove();
            this.scale.remove();
            if (this.scaleGroup) {
                this.scaleGroup.removeValue(this.name);
                if (this.scaleGroup.getValueNames().length === 0) {
                    delete this.trendWidget.vars.scaleGroups[this.scaleGroup.groupId];
                }
                this.scaleGroup = null;
            }
        }
    };
}());

/**
 * Module to implement abstract DataSource template class
 *
 * @module visuals/controls/iqtrend/datasource
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.datasource",
        /** @lends module:visuals/controls/iqtrend/datasource */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const msgNotImplemented = "Abstract method has no implementation:";

    module.DataSource = class DataSource {
        /**
         * Creates an instance of DataSource.
         */
        constructor() {
            this._name = "AbstractDataSource";
            this._type = "abstract";
            this._hasLive = false;
            this._hasArchived = false;
            this._hasComments = false;
            this._hasAlarms = false;
            this._isLive = false;
            this._tStart = 0;
            this._tEnd = 0;
            this._interval = 0;
        }
        /**
         * read datasource name
         *
         * @readonly
         * @type {string}
         */
        get name() {
            return this._name;
        }
        /**
         * read datasource type
         *
         * @readonly
         * @type {string}
         */
        get type() {
            return this._type;
        }
        /**
         * check if datasource has live values
         *
         * @readonly
         * @type {boolean}
         */
        get hasLive() {
            return this._hasLive;
        }
        /**
         * check if datasource has archived values
         *
         * @readonly
         * @type {boolean}
         */
        get hasArchived() {
            return this._hasArchived;
        }
        /**
         * check if datasource has comments
         *
         * @readonly
         * @type {boolean}
         */
        get hasComments() {
            return this._hasComments;
        }

        /**
         * check if
         *
         * @readonly
         * @type {boolean}
         */
        get hasAlarms() {
            return this._hasAlarms;
        }
        /**
         * check if datasource is retrieving live values
         *
         * @readonly
         * @type {boolean}
         */
        get isLive() {
            return this._isLive;
        }
        /**
         * check current datasource step interval in ms
         *
         * @readonly
         * @type {number}
         */
        get interval() {
            return this._interval;
        }
        /**
         * enable disable live data if supported
         *
         * @param {boolean} live enable `true` or disable `false` live mode
         *
         */
        setLive(live) {
            if (!this._hasLive) {
                throw new Error("Data source does not support live mode");
            }
        }
        /**
         * retrieve datasource value settings
         *
         */
        getValueSettings() {
            throw new Error(`[${this.name}] ${msgNotImplemented} getValueSettings`);
        }
        /**
         * retrieve datasource trend settings
         */
        getTrendSettings() {
            throw new Error(`[${this.name}] ${msgNotImplemented} getTrendSettings`);
        }
        /**
         * set datasource options
         */
        setOptions(options, noUpdate) {
            throw new Error(`[${this.name}] ${msgNotImplemented} setOptions`);
        }
        /**
         * add values to current datasource selection
         *
         * @param {...string} values names of values to add
         */
        select(...values) {
            throw new Error(`[${this.name}] ${msgNotImplemented} select`);
        }
        /**
         * remove values from current datasource selection
         *
         * @param {...string} values names of values to remove
         */
        deselect(...values) {
            throw new Error(`[${this.name}] ${msgNotImplemented} deselect`);
        }
        /**
         * watch for datasource changes
         *
         * @param {function} callback function to call with datasource changes
         */
        watch(callback) {
            throw new Error(`[${this.name}] ${msgNotImplemented} watch`);
        }
        /**
         * reload datasource comments if supported
         *
         */
        reloadComments() {
            throw new Error(`[${this.name}] ${msgNotImplemented} reloadComments`);
        }
        /**
         * reload datasource alarms if supported
         */
        reloadAlarms() {
            throw new Error(`[${this.name}] ${msgNotImplemented} reloadAlarms`);
        }
    };
}());

/**
 * Module to implement WebIQ Server trend data-source
 *
 * @module visuals/controls/iqtrend/datasourceTrend
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.datasourceTrend",
        /** @lends module:visuals/controls/iqtrend/datasourceTrend */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const { DataSource } = shmi.requires("visuals.controls.iqtrend.datasource"),
        { taskStates } = shmi.requires("visuals.controls.iqtrend.common");

    /**
     * get maximum value
     *
     * @param {object} self datasource reference
     * @param {number[][]} array of timestamp, value pairs
     * @returns {number} maximum value or `null` if none found
    */
    function getMax(self, values) {
        let max = null;
        if (Array.isArray(values)) {
            //remove outdated values that are no longer needed
            if (values.length >= 2 && (values[0][0] < self.data.start) && (values[1][0] <= self.data.start)) {
                values.shift();
            }
            values.forEach(([timestamp, value]) => {
                if (typeof value === "number") {
                    if (max === null) {
                        max = value;
                    } else if (value > max) {
                        max = value;
                    }
                }
            });
        }
        return max;
    }

    /**
     * get minimum value
     *
     * @param {object} self datasource reference
     * @param {number[][]} array of timestamp, value pairs
     * @returns {number} minimum value or `null` if none found
    */
    function getMin(self, values) {
        let min = null;
        if (Array.isArray(values)) {
            //remove outdated values that are no longer needed
            if (values.length >= 2 && (values[0][0] < self.data.start) && (values[1][0] <= self.data.start)) {
                values.shift();
            }
            values.forEach(([timestamp, value]) => {
                if (typeof value === "number") {
                    if (min === null) {
                        min = value;
                    } else if (value < min) {
                        min = value;
                    }
                }
            });
        }
        return min;
    }

    /**
     * sort values ascending by timestamp & filter values out of dataset range
     *
     * @param {object} self datasource reference
     * @param {string} itemName name of item
     * @param {string|null} property name of item property or `null` if item value
     * @param {number[][]} array of timestamp-value pairs
     */
    function sortAndFilterValues(self, itemName, property) {
        const currentValues = self.data.values[itemName];

        //sort according to timestamp
        currentValues[property].sort((a, b) => a[0] - b[0]);
        //filter old data & duplicate timestamps
        currentValues[property] = currentValues[property].filter((value, idx, valuesArray) => {
            let lastBeforeStart = true,
                firstAfterEnd = true;

            //filter values not in current range
            if (value[0] < self.data.start) {
                if (lastBeforeStart && (!Array.isArray(valuesArray[idx + 1]) || (valuesArray[idx + 1][0] >= self.data.start))) {
                    lastBeforeStart = false;
                    return true;
                }
                return false;
            } else if (value[0] > self.data.end) {
                if (firstAfterEnd) {
                    firstAfterEnd = false;
                    return true;
                }
                return false;
            }

            //filter duplicates
            if (idx === 0) {
                return true;
            }

            const isDuplicate = value[0] === valuesArray[idx - 1][0];
            if (isDuplicate) {
                log("Duplicate datapoint:", property === "item" ? itemName : `${itemName}.${property}`, value[0], `${value[1]} (${currentValues[property][idx - 1][1]})`);
            }

            return !isDuplicate;
        });

        return currentValues[property];
    }

    /**
     * sort markers ascending by timestamp & filter markers out of dataset range
     *
     * @param {object} self datasource reference
     * @param {string} markerType marker type
     * @returns {object[]} array of markers
     */
    function sortAndFilterMarkers(self, markerType) {
        let markers = self.data.markers[markerType];
        //sort according to timestamp
        markers.sort((a, b) => a.timestamp - b.timestamp);
        //filter old data
        markers = markers.filter((marker) => self.data.start <= marker.timestamp && marker.timestamp <= self.data.end);
        self.data.markers[markerType] = markers;

        return markers;
    }

    /**
     * insert loaded markers into dataset
     *
     * @param {object} self datasource reference
     * @param {object[]} markerData markers to insert
     * @param {string} markerType marker type
     */
    function insertMarkers(self, markerData, markerType) {
        const insertIds = markerData.map((marker) => marker.id);
        self.data.markers[markerType] = self.data.markers[markerType].filter((marker) => !insertIds.includes(marker.id));
        markerData.forEach((marker) => {
            self.data.markers[markerType].push(marker);
        });
        sortAndFilterMarkers(self, markerType);
    }

    /**
     * insert loaded item values into dataset
     *
     * @param {object} self datasource reference
     * @param {object} itemData item data
     */
    function insertItemValues(self, itemData) {
        initValues(self, itemData.item_alias);
        const currentValues = self.data.values[itemData.item_alias];
        Object.keys(itemData.values).forEach((key) => {
            itemData.values[key].map((value) => [value[0], processTrendData(self, itemData.item_alias, value[1])]).forEach((value) => {
                currentValues[key].push(value);
            });
            sortAndFilterValues(self, itemData.item_alias, key);
        });
    }

    /**
     * initialize item value storage
     *
     * @param {object} self datasource reference
     * @param {string} itemName item name
     */
    function initValues(self, itemName) {
        if (!self.data.values[itemName]) {
            self.data.values[itemName] = {
                item: [],
                display_min: [],
                display_max: [],
                display_trend_min: [],
                display_trend_max: [],
                prewarn_min: [],
                prewarn_max: [],
                tolerance_upper: [],
                tolerance_lower: [],
                warn_min: [],
                warn_max: [],
                stepsize: []
            };
        }
    }

    function isUnitClass(unitText) {
        if (typeof unitText === "string") {
            if (unitText.trim() === "") {
                return false;
            }
        }
        return (!isNaN(unitText));
    }

    function processTrendData(self, valueName, dataValue) {
        const uc = shmi.requires("visuals.tools.unitClasses"),
            valueSettings = self._valueSettings[valueName];

        if (dataValue !== null && valueSettings) {
            if (isUnitClass(valueSettings.unit)) {
                const unitClass = parseInt(valueSettings.unit),
                    adapter = uc.getSelectedAdapter(unitClass);

                if (adapter) {
                    dataValue = adapter.outFunction(dataValue);
                }
            }
        }
        return dataValue;
    }

    /**
     * recalculate value limits
     *
     * @param {object} self datasource reference
     * @param {string} itemName item name
     */
    function updateLimits(self, itemName) {
        initValues(self, itemName);
        const itemData = self.data.values[itemName];
        const valueSettings = self._valueSettings[itemName];
        if (!valueSettings) {
            return;
        }
        if (valueSettings.limits.mode === "manual" && (typeof valueSettings.limits.min !== "number" || (typeof valueSettings.limits.max !== "number"))) {
            valueSettings.limits.mode = "auto";
            console.info(`[iq-trend] changed limit mode from 'manual' to 'auto' due to missing limit specification.`);
        }

        if (valueSettings.limits.mode === "config") {
            const displayMin = getMin(self, itemData.display_trend_min.length ? itemData.display_trend_min : itemData.display_min),
                displayMax = getMax(self, itemData.display_trend_max.length ? itemData.display_trend_max : itemData.display_max);

            valueSettings.min = displayMin !== null ? displayMin : null;
            valueSettings.max = displayMax !== null ? displayMax : null;
        }

        if (valueSettings.limits.mode === "auto" || (valueSettings.limits.mode === "config" && (valueSettings.min === null || valueSettings.max === null))) {
            let autoMin = null,
                autoMax = null;
            if (valueSettings.drawTolerance) {
                let tolMin = getMin(self, itemData.tolerance_lower),
                    tolMax = getMax(self, itemData.tolerance_upper);

                autoMin = Math.min(...[tolMin, getMin(self, itemData.item)].filter((v) => v !== null));
                if (autoMin === Number.POSITIVE_INFINITY) {
                    autoMin = null;
                }
                autoMax = Math.max(...[tolMax, getMax(self, itemData.item)].filter((v) => v !== null));
                if (autoMax === Number.NEGATIVE_INFINITY) {
                    autoMax = null;
                }
            } else {
                autoMin = getMin(self, itemData.item);
                autoMax = getMax(self, itemData.item);
            }

            //adjust automatic limits for configured scale stepping
            if (typeof valueSettings.limits.step === "number") {
                if (autoMin !== null) {
                    autoMin = Math.floor(autoMin / valueSettings.limits.step) * valueSettings.limits.step;
                }

                if (autoMax !== null) {
                    autoMax = Math.ceil(autoMax / valueSettings.limits.step) * valueSettings.limits.step;
                }
            }
            valueSettings.min = autoMin === null ? typeof valueSettings.min === "number" ? valueSettings.min : null : autoMin;
            valueSettings.max = autoMax === null ? typeof valueSettings.max === "number" ? valueSettings.max : null : autoMax;
        } else if (valueSettings.limits.mode === "manual") { //manual mode
            valueSettings.min = valueSettings.limits.min;
            valueSettings.max = valueSettings.limits.max;
        }

        if (valueSettings.data_type === "string" || valueSettings.min === null || valueSettings.max === null) { //set limits to 0/1 for strings to not break scale functionality
            valueSettings.min = 0;
            valueSettings.max = 1;
        } else if (typeof valueSettings.min === "number" && valueSettings.min === valueSettings.max) { //adjust calculated scale for static values
            valueSettings.min -= 0.5;
            valueSettings.max += 0.5;
        }
    }

    /**
     * load recorded data
     *
     * @param {object} self datasource reference
     * @param {object} parameters load parameters
     * @param {object} parameters.self datasource reference
     * @param {number} parameters.tStart start timestamp in ms
     * @param {number} parameters.tEnd end timestamp in ms
     * @param {object} taskRef task reference
     */
    async function loadData({ self, tStart, tEnd }, taskRef) {
        const { TrendManager2: tm } = shmi.requires("visuals.core"),
            queryData = await tm.query(self._name, self.selectedValues, tStart, tEnd, self._interval);

        if (taskRef.state !== taskStates.CANCELED) {
            queryData.forEach((itemData) => {
                if (self.selectedValues.includes(itemData.item_alias)) {
                    self._valueSettings[itemData.item_alias] = self._valueSettings[itemData.item_alias] || {
                        limits: {
                            mode: "config", //"config", "auto", "manual"
                            min: null,
                            max: null
                        }
                    };

                    const valueSettings = self._valueSettings[itemData.item_alias];
                    valueSettings.interval = itemData.resolution;

                    insertItemValues(self, itemData);

                    updateLimits(self, itemData.item_alias);
                }
            });
            return Promise.resolve().then(() => {
                notifyWatchers(self);
            });
        }

        return null;
    }

    /**
     * load trend info
     *
     * @param {object} parameters info parameters
     * @param {object} parameters.self datasource reference
     * @param {object} taskRef task reference
     */
    async function loadInfo({ self }, taskRef) {
        const { TrendManager2: tm } = shmi.requires("visuals.core"),
            info = await tm.getInfo(self._name, self.selectedValues);

        if (taskRef.state !== taskStates.CANCELED) {
            self._trendSettings = self._trendSettings || {};
            self._trendSettings.start = info.record_first;
            self._trendSettings.end = info.record_last;
            self._trendSettings.trend_id = info.trend_id;
            info.items.forEach((item) => {
                self._valueSettings[item.name] = self._valueSettings[item.name] || {
                    limits: {
                        mode: "config", //"config", "auto", "manual"
                        min: null,
                        max: null
                    }
                };
                Object.keys(item).forEach((itemKey) => {
                    self._valueSettings[item.name][itemKey] = item[itemKey];
                });
            });
            self._infoSet = true;
        }
    }
    /**
     * load comment data
     *
     * @param {object} parameters thread parameters
     * @param {object} parameters.self datasource reference
     * @param {number} parameters.tStart start timestamp
     * @param {number} parameters.tEnd end timestamp
     * @param {object} taskRef task reference
     */
    async function loadThreads({ self, tStart, tEnd }, taskRef) {
        const threads = shmi.requires("visuals.tools.threads"),
            response = typeof self._trendSettings.trend_id === "undefined" ?
                { threads: [] } :
                await threads.list("trend", self._trendSettings.trend_id, { tStart, tEnd, loadUserDisplayName: true });

        if (taskRef.state !== taskStates.CANCELED) {
            insertMarkers(self, response.threads.map((thread) => ({
                timestamp: thread.reference_timestamp,
                id: thread.thread_id,
                type: "comment"
            })), "comment");

            return Promise.resolve().then(() => {
                notifyWatchers(self, {
                    comments: true
                });
            });
        }

        return null;
    }

    /**
     * update data after option change
     *
     * @param {object} self datasource reference
     * @param {object} parameters update settings
     * @param {boolean} parameters.timeChanged start, end or duration changed
     * @param {boolean} parameters.intervalChanged interval/resolution changed
     * @param {boolean} parameters.limitsChanged limit settings changed
    */
    async function updateData(self, {
        timeChanged,
        intervalChanged,
        limitsChanged
    }) {
        if (intervalChanged) {
            if (self.isLive) {
                await disableLive(self);
                await enableLive(self);
            } else {
                reloadAll(self, true);
            }
        } else if (timeChanged) {
            const tTriggerStart = Math.floor(self._tStart - (self._duration / 4)),
                tStart = Math.floor(self._tStart - (self._duration / 2));

            const now = Date.now(),
                tTriggerEnd = Math.min(now, Math.ceil(self._tEnd + (self._duration / 4))),
                tEnd = Math.min(now, Math.ceil(self._tEnd + (self._duration / 2)));

            if (tEnd < self.data.start || tStart > self.data.end) { //jump to new time
                reloadAll(self, true);
            } else if (tTriggerStart < self.data.start) { //backward drag
                loadSlice(self, tStart, self.data.start);
            } else if (tTriggerEnd > self.data.end) { //forward drag
                loadSlice(self, self.data.end, tEnd);
            }
        } else if (limitsChanged) {
            notifyWatchers(self);
        }
    }

    /**
     * reload trend data
     *
     * @param {object} self datasource reference
     * @param {boolean} noInfo if set to `true`, do not retrieve trend info
     * @param {boolean} loadLive load data in live mode (sets indefinite end timestamp)
     */
    function reloadAll(self, noInfo = false, loadLive = false) {
        const { TrendTask } = shmi.requires("visuals.controls.iqtrend.trendtask");
        //cancel all existing tasks
        self.queue.getTasks().forEach((task) => {
            if (!noInfo) {
                task.state = taskStates.CANCELED;
            } else if (task.type !== "info") {
                task.state = taskStates.CANCELED;
            }
        });

        if (!noInfo || !self._infoSet) {
            //request trend info
            const infoTask = new TrendTask("info", { self: self }, loadInfo);
            self.queue.queueTask(infoTask);
        }

        //request trend data
        const now = Date.now();
        const tStart = Math.floor(self._tStart - (self._duration / 2)),
            tEnd = Math.min(now, Math.ceil(self._tEnd + (self._duration / 2)));
        self.data.start = tStart;
        if (loadLive) {
            self.data.end = Number.MAX_SAFE_INTEGER;
        } else {
            self.data.end = tEnd;
        }

        self.data.values = {};
        const loadTask = new TrendTask("load", {
            self,
            tStart,
            tEnd
        }, loadData);
        self.queue.queueTask(loadTask);
        if (self._loadComments) {
            self.data.markers.comment = [];
            const threadsTask = new TrendTask("threads", {
                self,
                tStart,
                tEnd
            }, loadThreads);
            self.queue.queueTask(threadsTask);
        }
    }

    /**
     * load partial trend data (panning)
     *
     * @param {object} self datasource reference
     * @param {number} tStart start timestamp in ms
     * @param {number} tEnd end timestamp in ms
     */
    function loadSlice(self, tStart, tEnd) {
        const { TrendTask } = shmi.requires("visuals.controls.iqtrend.trendtask");
        if (tStart < self.data.start) {
            self.data.start = tStart;
            if (self.data.end > tStart + 3 * self._duration) {
                self.data.end = tStart + 2 * self._duration;
            }
        } else if (tEnd > self.data.end) {
            self.data.end = tEnd;
            if (self.data.start < tEnd - 3 * self._duration) {
                self.data.start = tEnd - 2 * self._duration;
            }
        }

        const loadTask = new TrendTask("load", {
            self,
            tStart,
            tEnd
        }, loadData);
        self.queue.queueTask(loadTask);
        if (self._loadComments) {
            const threadsTask = new TrendTask("threads", {
                self,
                tStart,
                tEnd
            }, loadThreads);
            self.queue.queueTask(threadsTask);
        }
    }

    /**
     * notify datasource watcher-callbacks
     *
     * @param {object} self datasource reference
     * @param {object} options notification options
     * @param {boolean} options.loadingState loading state changed
     * @param {boolean} options.comments comment data changed
     * @param {alarms} options.alarms alarm data changed
     */
    function notifyWatchers(self, options = {
        loadingState: false,
        comments: false,
        alarms: false
    }) {
        if (options.loadingState) {
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    loading: self.queue.loading
                });
            });
        } else if (options.comments) {
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    comments: self.data.markers.comment
                });
            });
        } else if (options.alarms) {
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    alarms: self.data.markers.alarm
                });
            });
        } else {
            //..update watchers with current state
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    info: {
                        range: self._trendSettings,
                        values: self._valueSettings
                    },
                    data: self.data
                });
            });
        }
    }

    /**
     * insert values from live trend subscription into dataset
     *
     * @param {object} self datasource reference
     * @param {object[]} values live data values
     */
    function insertLiveValues(self, values) {
        const currentValues = self.data.values;
        let endTs = Date.now();
        values.forEach((itemValue) => {
            if (!self.selectedValues.includes(itemValue.item)) {
                return;
            }
            if (itemValue.timestamp < endTs) {
                endTs = itemValue.timestamp;
            }
        });

        const newStartTs = endTs - 2 * self._duration;
        if (newStartTs > self.data.start) {
            self.data.start = newStartTs;
        }

        const itemNames = [];

        values.forEach((itemValue) => {
            if (!self.selectedValues.includes(itemValue.item)) {
                return;
            }
            if (!itemNames.includes(itemValue.item)) {
                itemNames.push(itemValue.item);
            }
            initValues(self, itemValue.item);
            const property = itemValue.property === null ? "item" : itemValue.property;
            currentValues[itemValue.item][property].push([itemValue.timestamp, processTrendData(self, itemValue.item, itemValue.value)]);
            sortAndFilterValues(self, itemValue.item, property);
        });

        itemNames.forEach((itemName) => {
            updateLimits(self, itemName);
        });
    }

    /**
     * enable live trend subscription
     *
     * @param {object} self datasource reference
     */
    async function enableLive(self, reloadInfo = false) {
        self._isLive = true;
        ++self._busy;
        self.setOptions({ tStart: Date.now() - self._duration }, true);

        try {
            const { TrendManager2: tm } = shmi.requires("visuals.session");
            const newSubscriptionId = await tm.subscribe(self._name, self.selectedValues, self._interval, (values) => {
                if (self._subscriptionId === newSubscriptionId) {
                    //insert values
                    insertLiveValues(self, values);
                    notifyWatchers(self);
                }
            });
            // If a previous subscription id exists, make sure to cancel the
            // now outdated subscription.
            // Important: self._subscriptionId can change while waiting on
            // tm.subscribe to complete. Don't rely on previously read values
            // or write blindly to it.
            const oldSubscriptionId = self._subscriptionId;
            self._subscriptionId = newSubscriptionId;

            if (oldSubscriptionId) {
                await tm.unsubscribe(oldSubscriptionId);
            }
        } catch (err) {
            console.error("[trend2.subscribe] Error subscribing live trend:", err);
        }

        --self._busy;
        // Make sure _isLive is updated to the correct value in case someone
        // has destroyed the subscription in the meantime or the subscription
        // has failed.
        self._isLive = self._subscriptionId !== null;
        reloadAll(self, !reloadInfo, true);
    }

    /**
     * disable live trend subscription
     *
     * @param {object} self datasource reference
     */
    async function disableLive(self) {
        const { TrendManager2: tm } = shmi.requires("visuals.session");
        //close active subscription
        self._isLive = false;
        if (self._subscriptionId === null) {
            return;
        }
        ++self._busy;
        try {
            // Set _subscriptionId to null immediately as it's value could've
            // changed while waiting on tm.unsubscribe to complete, causing us
            // to null out the wrong value.
            const subscriptionId = self._subscriptionId;
            self._subscriptionId = null;
            await tm.unsubscribe(subscriptionId);
            const currentValues = self.data.values;
            let endTs = Date.now();
            Object.entries(currentValues).forEach(([name, values]) => {
                if (values.length) {
                //check last timestamp of each value and use earliest timestamp as end of dataset
                    const lastTimestamp = values[values.length - 1][0];
                    if (lastTimestamp < endTs) {
                        endTs = lastTimestamp;
                    }
                }
            });
            self.data.end = endTs;
        } catch (err) {
            console.error("[trend2.unsubscribe] Error unsubscribing live trend:", err);
        }

        --self._busy;
        // Make sure _isLive is updated to the correct value in case someone
        // has created a new subscription in the meantime or the subscription
        // could not be destroyed.
        self._isLive = self._subscriptionId !== null;
    }

    /**
     * WebIQ Server trend data-source
     *
     * @constructor
     * @param {string} name data-source trend name
    */
    module.DataSourceTrend = class DataSourceTrend extends DataSource {
        constructor(name) {
            super();
            const { TaskQueue } = shmi.requires("visuals.controls.iqtrend.taskqueue");
            this._name = name;
            this._type = "trend";

            this._hasLive = true;
            this._hasArchived = true;
            this._hasComments = true;
            this._hasAlarms = false;

            this._isLive = false;
            this._tStart = 0;
            this._duration = 0;
            this._interval = 0;
            this._tEnd = this._tStart + this._duration;
            this.data = {
                start: 0,
                end: 0,
                values: {},
                markers: {
                    comment: [],
                    alarm: []
                }
            };
            this._infoSet = false;
            this._loadComments = false;
            this.selectedValues = [];
            this.watchers = [];
            this.queue = new TaskQueue();
            this.queue.watch(() => {
                notifyWatchers(this, {
                    loadingState: true
                });
            });

            this._loadQueue = [];
            this._valueSettings = {};
            this._trendSettings = {
                start: 0,
                end: 0
            };

            this._busy = 0;
            this._subscriptionId = null;
        }
        /**
         * get value settings
         *
         * @returns {object} value settings
         */
        getValueSettings() {
            return shmi.cloneObject(this._valueSettings);
        }
        /**
         * get trend settings
         *
         * @returns {object} trend settings
         */
        getTrendSettings() {
            return shmi.cloneObject(this._trendSettings);
        }
        /**
         * set datasource options
         *
         * @param {object} options datasource options
         * @param {number} [options.tStart] start timestamp in ms
         * @param {number} [options.duration] display duration in ms
         * @param {number} [options.interval] minimum step between data points in ms
         * @param {object} [options.tolerance] tolerance settings
         * @param {boolean} [options.tolerance.drawTolerance] enable (`true`) or disable (`false`) display of tolerances
         * @param {string} [options.tolerance.name] name of value to apply tolerance settings to
         * @param {object} [options.limits] value limit settings
         * @param {string} [options.limits.name] name of value to apply limit settings to
         * @param {string} [options.limits.mode] limit mode, either `"config"` (use item settings), `"manual"` (use supplied 'min', 'max' values) or `"auto"` (automatically calculate limits)
         * @param {number} [options.limits.step] if 'mode' is set to `"auto"` calculate min- & max-values as multiples of `step`
         * @param {number} [options.limits.min] if 'mode' is set to `"manual"` use this as min-value
         * @param {number} [options.limits.max] if 'mode' is set to `"manual"` use this as max-value
         * @param {boolean} [options.loadComments] enable (`true`) or disable (`false`) loading of comment data
         * @param {boolean} noUpdate prevent update of loaded data (used to advance current time in live mode)
         */
        setOptions(options, noUpdate = false) {
            if (!options) {
                throw new Error("No options provided");
            }

            let timeChanged = false,
                intervalChanged = false,
                limitsChanged = false;

            if (typeof options.tStart === "number" && this._tStart !== options.tStart) {
                this._tStart = options.tStart;
                timeChanged = true;
            }

            if (typeof options.duration === "number" && this._duration !== options.duration) {
                this._duration = options.duration;
                timeChanged = true;
            }

            if (typeof options.interval === "number" && this._interval !== options.interval) {
                this._interval = Math.max(1, options.interval);
                intervalChanged = true;
            }

            if (timeChanged) {
                this._tEnd = this._tStart + this._duration;
            }

            if (options.tolerance && typeof options.tolerance.drawTolerance === "boolean" && typeof options.tolerance.name === "string") {
                this._valueSettings[options.tolerance.name] = this._valueSettings[options.tolerance.name] || {
                    limits: {
                        mode: "config", //"config", "auto", "manual"
                        min: null,
                        max: null
                    }
                };
                this._valueSettings[options.tolerance.name].drawTolerance = options.tolerance.drawTolerance;
            }

            if (options.limits && typeof options.limits === "object" && typeof options.limits.name === "string") {
                const limitsOpt = shmi.cloneObject(options.limits);
                delete limitsOpt.name;
                this._valueSettings[options.limits.name] = this._valueSettings[options.limits.name] || {};
                this._valueSettings[options.limits.name].limits = limitsOpt;
                updateLimits(this, options.limits.name);
                limitsChanged = true;
            }

            if (options.loadComments && typeof options.loadComments === "boolean") {
                this._loadComments = true;
            }

            if (!noUpdate) {
                updateData(this, {
                    timeChanged,
                    intervalChanged,
                    limitsChanged
                });
            }
        }
        /**
         * select values for trend selection
         *
         * @param {...string} values values to add to trend selection
         */
        select(...values) {
            let valueSelected = false;
            //add values to selection
            values.forEach((value) => {
                if (!this.selectedValues.includes(value)) {
                    log("Add value to selection:", value);
                    this.selectedValues.push(value);
                    valueSelected = true;
                }
            });

            if (valueSelected) {
                if (this.isLive) {
                    disableLive(this).then(() => {
                        enableLive(this, true);
                    });
                } else {
                    reloadAll(this);
                }
            }
        }
        /**
         * deselect values from trend selection
         *
         * @param {...string} values values to remove from trend selection
         */
        deselect(...values) {
            let valueDeselected = false;
            //remove values from selection
            values.forEach((value) => {
                if (this.selectedValues.includes(value)) {
                    log("Remove value from selection:", value);
                    this.selectedValues.splice(this.selectedValues.indexOf(value), 1);
                    delete this.data.values[value];
                    delete this._valueSettings[value];
                    valueDeselected = true;
                }
            });

            if (valueDeselected && this.selectedValues.length > 0) {
                if (this.isLive) {
                    disableLive(this).then(() => {
                        enableLive(this);
                    });
                } else {
                    reloadAll(this);
                }
            } else if (this.selectedValues.length === 0) {
                if (this.isLive) {
                    disableLive(this).then(() => {
                        notifyWatchers(this);
                    });
                } else {
                    notifyWatchers(this);
                }
            }
        }
        /**
         * enable or disable live mode
         *
         * @param {boolean} isLive live mode state, `true` to enable, `false` to disable
         * @return {boolean} live mode state
         */
        setLive(isLive) {
            if (isLive === this._isLive) {
                return this._isLive;
            }
            super.setLive();
            if (this._busy) {
                return this._isLive;
            }
            if (isLive && this.selectedValues.length > 0) {
                enableLive(this);
            } else if (this._subscriptionId) {
                //close active subscription
                disableLive(this);
            }
            return this._isLive;
        }
        /**
         * watch for datasource property changes
         *
         * @param {function} callback function to run when datasource properties change
         * @return {object} watch token to stop receiving property changes
         */
        watch(callback) {
            this.watchers.push(callback);
            return {
                unwatch: () => {
                    const idx = this.watchers.indexOf(callback);
                    if (idx !== -1) {
                        this.watchers.splice(idx, 1);
                    }
                }
            };
        }
        /**
         * reload comments for currently displayed time period
         */
        reloadComments() {
            const { TrendTask } = shmi.requires("visuals.controls.iqtrend.trendtask"),
                tStart = Math.floor(this._tStart - (this._duration / 2)),
                tEnd = Math.min(Date.now(), Math.ceil(this._tEnd + (this._duration / 2)));

            if (this._loadComments) {
                this.data.markers.comment = [];
                const threadsTask = new TrendTask("threads", {
                    self: this,
                    tStart,
                    tEnd
                }, loadThreads);
                this.queue.queueTask(threadsTask);
            }
        }
    };
}());

/**
 * Module to implement value graph for iq-trend.
 *
 * @module visuals/controls/iqtrend/graph
*/
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.graph",
        /** @lends module:visuals/controls/iqtrend/graph */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas element size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
    }

    /**
     * get canvas coordinates from value space coordinates
     *
     * @param {object} self instance reference
     * @param {number|null} x x-coordiante
     * @param {number|null} y y-coordinate
     * @return {object} coordinates on canvas
     */
    function getCoordinates(self, x, y) {
        return {
            x: x === null ? null : (x - self.minX) * self.scaleX + 1,
            y: y === null ? null : self.height - (y - self.minY) * self.scaleY + 1
        };
    }

    /**
     * get line segments for time period displayed on graph
     *
     * @param {object} self instance reference
     * @param {string|null} [property=null] value property or `null` for main value
     * @return {object[]} line segments
     */
    function getSegments(self, property = null) {
        const values = self.values.filter((value) => value.property === property);
        let firstIndex = values.findIndex((value) => value.x >= self.minX);
        if (firstIndex > 0) {
            firstIndex -= 1;
        } else if (firstIndex === -1 && values.length) {
            firstIndex = values.length - 1;
        }
        let segments = [];
        if (firstIndex >= 0) {
            for (let i=firstIndex; i < values.length; i++) {
                segments.push({
                    x1: values[i].x,
                    x2: null,
                    y: typeof values[i].y === "boolean" ? values[i].y ? 1 : 0 : values[i].y
                });
                if (segments.length > 1) {
                    let lastSegment = segments[segments.length - 2];
                    const segmentDuration = values[i].x - lastSegment.x1;
                    let numIntervals = Math.floor(segmentDuration / self.interval);
                    const segmentRemainder = segmentDuration - numIntervals * self.interval;
                    if (!self.discrete && numIntervals > 0 && segmentRemainder === 0) {
                        numIntervals -= 1;
                    }
                    lastSegment.x2 = lastSegment.x1 + numIntervals * self.interval;
                    if (self.discrete) {
                        lastSegment.x2 += segmentRemainder;
                    }
                }
                if (values[i].x >= self.maxX) {
                    break;
                }
            }

            if (segments.length) {
                if (segments[0].x1 < self.minX && ((segments[0].x2 > segments[0].x1) || (segments[0].x2 === null))) {
                    segments[0].x1 = self.minX;
                }

                if (segments[segments.length - 1].x1 < self.maxX) {
                    if (self.dataset.trendWidget.isLive() && (segments[segments.length - 1].x1 + (self.interval * 2)) <= self.maxX) { //live mode
                        //with auto-continuation:
                        segments[segments.length - 1].x2 = segments[segments.length - 1].x1 + (Math.floor((self.maxX - segments[segments.length - 1].x1) / self.interval) * self.interval);

                        //no auto-continuation:
                        //segments[segments.length - 1].x2 = segments[segments.length - 1].x1;
                    } else if (!self.dataset.trendWidget.isLive() && segments[segments.length - 1].x1 < self.maxX) { //archived data, last value < max-X
                        segments[segments.length - 1].x2 = segments[segments.length - 1].x1 + (Math.ceil((self.maxX - segments[segments.length - 1].x1) / self.interval) * self.interval);
                    } else { //archived data, last value >= max-X
                        segments[segments.length - 1].x2 = segments[segments.length - 1].x1;
                    }
                } else { //archived data
                    segments = segments.filter((segment, idx) => idx === 0 || segment.x1 <= self.maxX || segments[idx - 1].x2 < self.maxX);

                    if (segments.length === 0) {
                        return [];
                    }

                    const lastSegment = segments[segments.length - 1],
                        prevSegment = segments[segments.length - 2];
                    if (prevSegment && lastSegment.x1 > self.maxX) {
                        const dt1 = lastSegment.x1 - prevSegment.x2,
                            dv = (lastSegment.y - prevSegment.y) / dt1,
                            dt2 = self.maxX - prevSegment.x2;
                        lastSegment.x1 = lastSegment.x2 = self.maxX;
                        lastSegment.y = prevSegment.y + (dt2 * dv);
                    } else if (lastSegment.x2 > self.maxX && lastSegment.x2 > lastSegment.x1) {
                        lastSegment.x2 = self.maxX;
                    }
                }
            }
        }

        return segments;
    }

    /**
     * draw line from line segments
     *
     * @param {object} self instance reference
     * @param {object} style style options
     * @param {object[]} segments line segments
     * @return {object[][]} drawn lines
     */
    function drawLine(self, style, segments) {
        if (!segments.length) {
            return [];
        }
        const context = self.context;
        context.lineWidth = style.lineWidth ? style.lineWidth : 1;
        context.strokeStyle = style.strokeStyle ? style.strokeStyle : "rgb(0, 0, 0)";
        if (Array.isArray(style.lineDash) && style.lineDash.length) {
            context.setLineDash(style.lineDash);
        } else {
            context.setLineDash([]);
        }
        context.beginPath();
        let lastValue = null;

        const lineSegments = [];
        let currentLine = [];
        segments.forEach((segment) => {
            if (typeof segment.y !== "number" || segment.y === null || segment.x1 === null || segment.x2 === null) {
                lastValue = null;
                if (currentLine.length) {
                    lineSegments.push(currentLine);
                    currentLine = [];
                }
                return;
            }
            const start = getCoordinates(self, segment.x1, segment.y),
                end = getCoordinates(self, segment.x2, null);

            if (lastValue === null) {
                log(`moveTo ${start.x}, ${start.y}`);
                context.moveTo(start.x, start.y);
            } else {
                log(`lineTo ${start.x}, ${start.y}`);
                context.lineTo(start.x, start.y);
            }
            lastValue = start.y;
            log(`lineTo ${end.x}, ${start.y}`);
            context.lineTo(end.x, start.y);
            currentLine.push({ x1: start.x, x2: end.x, y: start.y });
        });
        if (currentLine.length) {
            lineSegments.push(currentLine);
        }
        log(`-> stroke`);
        context.stroke();
        return lineSegments;
    }

    /**
     * set style of line
     *
     * @param {object} self instance reference
     * @param {string} name either `"value"` or `"tolerance"
     * @param {object} style style options
     */
    function setLineStyle(self, name, style) {
        const lineStyle = self.style[name];
        if (lineStyle) {
            if (typeof style.lineWidth === "number" && style.lineWidth > 0) {
                lineStyle.lineWidth = style.lineWidth;
            }

            if (typeof style.strokeStyle === "string") {
                lineStyle.strokeStyle = style.strokeStyle;
            }

            if (typeof style.fillStyle === "string") {
                lineStyle.fillStyle = style.fillStyle;
            }

            if (Array.isArray(style.lineDash)) {
                lineStyle.lineDash = style.lineDash;
            }
        }
    }

    /**
     * find value of graph at specified timestamp
     *
     * @param {object[]} segments line segments
     * @param {number} timestamp timestamp in ms
     * @return {number|null} value at specified timestamp or `null` if none recorded
     */
    function findSegmentValue(segments, timestamp) {
        const start = segments.slice().reverse().find((segment) => segment.x1 <= timestamp),
            end = segments.find((segment) => segment.x2 >= timestamp);

        if (!start || !end) {
            return null;
        }

        if (start === end) {
            return start.y;
        }

        //do not try to interpolate string values
        if (typeof start.y === "string" || typeof end.y === "string") {
            return typeof start.y === "string" ? start.y : end.y;
        }

        const range = start.x2 - end.x1,
            tDiff = timestamp - start.x2,
            vDiff = end.y - start.y;

        return start.y - (tDiff / range) * vDiff;
    }

    module.Graph = class Graph {
        /**
         * Creates an instance of Graph.
         * @param {HTMLElement} element parent element to attach graph to
         * @param {object} options graph options
         */
        constructor(element, {
            externalDraw = false,
            x = {
                min: 0,
                max: 60000
            },
            y = {
                min: 0,
                max: 100
            },
            interval = 1000,
            discrete = false,
            style = {
                value: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    fillStyle: "rgb(0, 0, 0.05)",
                    lineDash: []
                },
                tolerance: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    lineDash: [5, 5]
                }
            },
            fillCurve = false,
            drawTolerance = false,
            dataset = null
        }) {
            this.values = [];
            this.canvas = document.createElement("canvas");
            this.parent = element;
            this.canvas.classList.add("iqel-graph-canvas");
            this.parent.appendChild(this.canvas);

            const rect = this.parent.getBoundingClientRect();
            this.width = rect.width - 2;
            this.height = rect.height - 2;
            this.style = {
                value: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    fillStyle: "rgb(0, 0, 0.05)",
                    lineDash: []
                },
                tolerance: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    lineDash: [5, 5]
                }
            };
            this.dataset = dataset;
            this.segments = {
                value: [],
                tolerance: {
                    min: [],
                    max: []
                }
            };
            this.setOptions({
                externalDraw,
                x,
                y,
                interval,
                discrete,
                style,
                fillCurve,
                drawTolerance
            }, false);
            updateSize(this);
            this.context = this.canvas.getContext("2d");
            let watcherUpdateId = 0;
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = entry.contentRect.width - 2;
                this.height = entry.contentRect.height - 2;
                this.scaleX = this.width / (this.maxX - this.minX);
                this.scaleY = this.height / (this.maxY - this.minY);
                updateSize(this);

                shmi.caf(watcherUpdateId);
                watcherUpdateId = shmi.raf(() => {
                    if (!this.parent) {
                        return; //cancel if detached from parent
                    }
                    this.watchers.forEach((w) => {
                        w({
                            graph: this,
                            dataset: this.dataset,
                            width: this.width,
                            height: this.height,
                            scale: {
                                x: this.scaleX,
                                y: this.scaleY
                            },
                            dimensions: {
                                x: {
                                    min: this.minX,
                                    max: this.maxX
                                },
                                y: {
                                    min: this.minY,
                                    max: this.maxY
                                }
                            }
                        });
                    });
                });

                this.draw();
            });
            this.watchers = [];
            this.observer.observe(this.parent);
            this.draw();
        }
        /**
         * set graph options after instantiation
         *
         * @param {object} options graph options
         * @param {boolean} [update=true] apply options & redraw graph immediately if `true`
         */
        setOptions(options, update = true) {
            let updateScale = false,
                updateDraw = false;

            if (typeof options.discrete === "boolean") {
                this.discrete = options.discrete;
                updateDraw = true;
            }

            if (options.y && typeof options.y.min === "number") {
                this.minY = options.y.min;
                updateScale = true;
            }

            if (options.y && typeof options.y.max === "number") {
                this.maxY = options.y.max;
                updateScale = true;
            }

            if (options.x && typeof options.x.min === "number") {
                this.minX = options.x.min;
                updateScale = true;
            }

            if (options.x && typeof options.x.max === "number") {
                this.maxX = options.x.max;
                updateScale = true;
            }

            if (typeof options.interval === "number" && options.interval > 0) {
                this.interval = options.interval;
                updateDraw = true;
            }

            if (typeof options.externalDraw === "boolean") {
                this.externalDraw = options.externalDraw;
                updateDraw = true;
            }

            if (typeof options.fillCurve === "boolean") {
                this.fillCurve = options.fillCurve;
                updateDraw = true;
            }

            if (typeof options.drawTolerance === "boolean") {
                this.drawTolerance = options.drawTolerance;
                updateDraw = true;
            }

            if (options.style) {
                if (options.style.value) {
                    setLineStyle(this, "value", options.style.value);
                }
                if (options.style.tolerance) {
                    setLineStyle(this, "tolerance", options.style.tolerance);
                }
            }

            if (updateScale) {
                this.scaleX = this.width / (this.maxX - this.minX);
                this.scaleY = this.height / (this.maxY - this.minY);
            }

            if (update && (updateDraw || updateScale)) {
                this.draw();
            }
        }
        /**
         * draw graph
         *
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            const segments = getSegments(this, null);
            this.segments.value = segments;
            const lineElements = drawLine(this, {
                lineWidth: this.style.value.lineWidth,
                strokeStyle: this.style.value.strokeStyle,
                lineDash: this.style.value.lineDash
            }, segments);

            if (this.fillCurve && lineElements.length) {
                this.context.fillStyle = this.style.value.fillStyle;
                lineElements.forEach((line) => {
                    this.context.beginPath();
                    line.forEach((segment, idx) => {
                        if (idx === 0) {
                            this.context.moveTo(segment.x1, segment.y);
                        } else {
                            this.context.lineTo(segment.x1, segment.y);
                        }
                        this.context.lineTo(segment.x2, segment.y);
                    });
                    const minY = this.height + 2;
                    this.context.lineTo(line[line.length - 1].x2, minY);
                    this.context.lineTo(line[0].x1, minY);
                    this.context.closePath();
                    this.context.fill();
                });
            }

            if (this.drawTolerance) {
                const tolLowerSegments = getSegments(this, "tolerance_lower");
                const tolUpperSegments = getSegments(this, "tolerance_upper");

                this.segments.tolerance.min = tolLowerSegments;
                this.segments.tolerance.max = tolUpperSegments;

                drawLine(this, {
                    lineWidth: this.style.tolerance.lineWidth,
                    strokeStyle: this.style.tolerance.strokeStyle,
                    lineDash: this.style.tolerance.lineDash
                }, tolLowerSegments);
                drawLine(this, {
                    lineWidth: this.style.tolerance.lineWidth,
                    strokeStyle: this.style.tolerance.strokeStyle,
                    lineDash: this.style.tolerance.lineDash
                }, tolUpperSegments);
            }
        }
        /**
         * get timestamp at specified x-coordinate of canvas
         *
         * @param {number} x x-coordinate
         * @return {number} timestamp in ms
         */
        getTimeAt(x) {
            x -= 1; //account for extra pixel

            const range = this.maxX - this.minX,
                time = this.minX + (x / this.width) * range;

            return time;
        }
        /**
         * get value at specified y-coordinate of canvas
         *
         * @param {number} y y-coordinate
         * @return {number} value at coordinate
         */
        getValueAtY(y) {
            y -= 1; //account for extra pixel

            const range = this.maxY - this.minY,
                pos = this.height - y,
                value = this.minY + (pos / this.height) * range;

            return value;
        }
        /**
         * get value of graph at specified timestamp
         *
         * @param {number} x timestamp in ms
         * @return {object} value & tolerances
         */
        getValue(x) {
            const value = findSegmentValue(this.segments.value, x),
                tolUpper = findSegmentValue(this.segments.tolerance.max, x),
                tolLower = findSegmentValue(this.segments.tolerance.min, x);

            return {
                value: typeof value === "number" || typeof value === "string" ? value : null,
                tolerance: {
                    min: typeof tolUpper === "number" ? tolUpper : null,
                    max: typeof tolLower === "number" ? tolLower : null
                }
            };
        }
        /**
         * get current graph dimensions
         *
         * @return {*}
         */
        getDimensions() {
            return {
                x: {
                    min: this.minX,
                    max: this.maxX
                },
                y: {
                    min: this.minY,
                    max: this.maxY
                },
                width: this.width,
                height: this.height
            };
        }
        /**
         * add single value to graph
         *
         * @param {number} x x value (timestamp in ms)
         * @param {number} y y value (recorded value)
         * @param {string} [property=null] property name or `null` for main value
         */
        addValue(x, y, property = null) {
            this.values.push({ x, y, property });
            this.values.sort((a, b) => a.x - b.x);
            if (!this.externalDraw) {
                this.draw();
            }
        }
        /**
         * add multiple values to graph
         *
         * @param {object[]} values values to add
         */
        addValues(values) {
            values.forEach(([x, y, property = null]) => {
                this.values.push({ x, y, property });
            });
            this.values.sort((a, b) => a.x - b.x);
            if (!this.externalDraw) {
                this.draw();
            }
        }
        /**
         * clear current graph values
         *
         */
        clearValues() {
            this.values = [];
        }
        /**
         * remove graph from parent
         *
         */
        remove() {
            if (this.parent) {
                this.clearValues();
                this.observer.disconnect();
                this.parent.removeChild(this.canvas);
                this.parent = null;
                this.watchers = [];
            }
        }
        /**
         * watch graph for dimension changes
         *
         * @param {function} callback notification callback
         * @return {object} reference to stop watching
         */
        watch(callback) {
            this.watchers.push(callback);
            return {
                unwatch: () => {
                    const idx = this.watchers.indexOf(callback);
                    if (idx !== -1) {
                        this.watchers.splice(idx, 1);
                    }
                }
            };
        }
    };
}());

/**
 * Module to implement overlay marker display (comments) for iq-trend
 *
 * @module visuals/controls/iqtrend/markerlayer
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.markerlayer",
        /** @lends module:visuals/controls/iqtrend/markerlayer */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update width & scale
     *
     * @param {object} self instance reference
     * @param {object} contentRect element dimensions
     */
    function updateSize(self, contentRect) {
        self.width = contentRect.width - 2;
        self.scale = self.width / (self.max - self.min);
    }

    /**
     * get template for marker type
     *
     * @param {object} self instance reference
     * @param {string} type marker type
     * @return {HTMLElement|null} marker template
     */
    function getMarkerTemplate(self, type) {
        return self.trendWidget.vars.markerTemplates[type] || null;
    }

    /**
     * get x-coordinate from timestamp
     *
     * @param {object} self instance reference
     * @param {number} x timestamp in ms
     * @return {number|null} x-coordinate or `null` if no timestamp supplied
     */
    function getXCoordinate(self, x) {
        return x === null ? null : (x - self.min) * self.scale + 1;
    }

    /**
     * shortcut for API requests
     *
     * @param {string} apiCommand api command
     * @param {object} parameters command parameters
     * @param {object} options request options
     * @return {Promise} promise resolving to request response
     */
    function request(apiCommand, parameters, options) {
        return shmi.visuals.session.ConnectSession.requestPromise(apiCommand, parameters, options);
    }

    /**
     * lookup element in marker template
     *
     * @param {object} markerRef marker reference
     * @param {string} elementId element id for marker reference
     * @param {string} elementAttribute 'data-ui' attribute to lookup
     * @return {boolean} `true` if found & mapped, `false` else
     */
    function lookupMarkerElement(markerRef, elementId, elementAttribute) {
        markerRef[elementId] = shmi.getUiElement(elementAttribute, markerRef.element);
        if (!markerRef[elementId]) {
            console.warn(`Widget template is missing '${elementAttribute}' element required to activate comment markers.`);
            return false;
        }
        return true;
    }

    /**
     * perform if marker info is closed
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    function onMarkerClose(self, markerRef) {
        markerRef.element.classList.remove("open");
        self.parent.classList.remove("active");
        markerRef.listenersContent.forEach((l) => {
            l.disable();
        });
        markerRef.listenersContent = [];
        self.currentMarker = null;
    }

    /**
     * update size of marker info
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    function updateMarkerSize(self, markerRef) {
        //reset existing property & class for calculation
        markerRef.info.classList.remove("reversed");
        markerRef.info.style.removeProperty("--trend-marker-info-width");

        const widgetStyle = getComputedStyle(self.trendWidget.element),
            varValue = parseInt(widgetStyle.getPropertyValue("--trend-marker-info-width").replace("px", "")),
            graphsRect = self.trendWidget.vars.elements.graphs.getBoundingClientRect(),
            markerRect = markerRef.element.getBoundingClientRect(),
            infoRect = markerRef.info.getBoundingClientRect(),
            infoOffset = Math.abs(markerRect.x - infoRect.x);

        if (markerRect.x <= graphsRect.x + (graphsRect.width / 2)) {
            //normal
            if (markerRect.x - infoOffset + varValue > graphsRect.x + graphsRect.width) {
                //adjust size
                const diffX = (markerRect.x - infoOffset + varValue) - (graphsRect.x + graphsRect.width);
                markerRef.info.style.setProperty("--trend-marker-info-width", `${varValue - diffX}px`);
            }
        } else {
            //reverse
            markerRef.info.classList.add("reversed");

            if (markerRect.x + infoOffset - varValue < graphsRect.x) {
                const diffX = graphsRect.x - (markerRect.x + infoOffset - varValue);
                markerRef.info.style.setProperty("--trend-marker-info-width", `${varValue - diffX}px`);
            }
        }
    }

    /**
     * load & set content of marker info
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    async function setMarkerContent(self, markerRef) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            marker = markerRef.marker,
            { currentUser } = shmi.requires("visuals.session.UserManager"),
            timestampFormat = shmi.localize(self.trendWidget.config.commentTimestampFormat || "$DD.$MM.$YYYY, $HH:$mm:$ss");

        try {
            const markerData = await request("thread.get", {
                thread_id: marker.id
            });

            //workaround - test if user may edit thread
            if (self.mayModifyThreads === null) {
                let mayEdit = false;
                try {
                    await request("thread.modify", { thread_id: -1, title: null, message: "" });
                } catch (err) {
                    if (!(err.category === "shmi:connect:api:generic" && err.errc === 4)) {
                        mayEdit = true;
                    }
                }
                self.mayModifyThreads = mayEdit;
            }

            if (markerData) {
                markerRef.title.textContent = markerData.message.title;
                markerRef.title.title = markerData.message.title;
                markerRef.text.textContent = markerData.message.message;
                const markerTime = typeof markerData.message.edited_timestamp === "number" ? markerData.message.edited_timestamp : markerData.message.timestamp,
                    timeText = formatDateTime(markerTime / 1000, timestampFormat);
                markerRef.time.textContent = timeText;
                markerRef.time.title = timeText;
                if (self.mayModifyThreads && currentUser.id === markerData.message.user_id) {
                    markerRef.element.classList.add("editable");
                }

                try {
                    const userInfo = await request("user.info", markerData.message.user_id);
                    if (userInfo.first_name && userInfo.first_name.length && userInfo.last_name && userInfo.last_name.length) {
                        markerRef.user.textContent = `${userInfo.first_name} ${userInfo.last_name}`;
                    } else {
                        markerRef.user.textContent = userInfo.username;
                    }
                } catch (userExc) {
                    console.error(`Error retrieving user information for user-id '${markerData.message.user_id}':`, userExc);
                    markerRef.user.textContent = markerData.message.user_id;
                }
            }
        } catch (threadExc) {
            console.error(`Error retrieving data for thread-ID '${marker.id}':`, threadExc);
        }
    }

    /**
     * perform when marker info is opened
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    async function onMarkerOpen(self, markerRef) {
        const marker = markerRef.marker;

        if (self.trendWidget.isLive()) {
            self.trendWidget.setLive(false);
        }

        if (self.currentMarker) {
            onMarkerClose(self, self.currentMarker.ref);
        }
        self.currentMarker = {
            data: marker,
            ref: markerRef
        };

        updateMarkerSize(self, markerRef);

        await setMarkerContent(self, markerRef);
        markerRef.element.classList.add("open");
        self.parent.classList.add("active");

        const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
            inputHandlerClose = {
                onPress: (x, y, event) => {
                    event.stopPropagation();
                },
                onRelease: (x, y, event) => {
                    event.stopPropagation();
                },
                onClick: (x, y, event) => {
                    event.stopPropagation();
                    onMarkerClose(self, markerRef);
                }
            },
            mhClose = new MouseListener(markerRef.close, inputHandlerClose),
            thClose = new TouchListener(markerRef.close, inputHandlerClose),
            mhParent = new MouseListener(self.parent, inputHandlerClose),
            thParent = new TouchListener(self.parent, inputHandlerClose);

        const inputHandlerEdit = {
                onPress: (x, y, event) => {
                    event.stopPropagation();
                },
                onRelease: (x, y, event) => {
                    event.stopPropagation();
                },
                onClick: async (x, y, event) => {
                    event.stopPropagation();
                    if (!self.trendWidget.isLocked()) {
                        const threads = shmi.requires("visuals.tools.threads"),
                            result = await threads.edit(marker.id, {
                                dialogTitle: "${iqtrend_edit_thread}",
                                titleLabel: "${iqtrend_threads_title}",
                                messageLabel: "${iqtrend_threads_message}",
                                deleteLabel: "${iqtrend_threads_delete_btn}",
                                deleteConfirmation: "${iqtrend_threads_delete_confirmation}",
                                applyLabel: "${iqtrend_threads_edit_btn}",
                                enterMessageNotification: "${iqtrend_threads_enter_message_notification}"
                            });
                        if (result !== null) {
                            if (result === -1) { //comment was deleted
                                onMarkerClose(self, markerRef);
                                self.trendWidget.vars.datasource.source.reloadComments();
                            } else {
                                await setMarkerContent(self, markerRef);
                            }
                        }
                    }
                }
            },
            mhEdit = new MouseListener(markerRef.edit, inputHandlerEdit),
            thEdit = new TouchListener(markerRef.edit, inputHandlerEdit);

        const inputHandlerContent = {
                onPress: (x, y, event) => {
                    event.stopPropagation();
                },
                onRelease: (x, y, event) => {
                    event.stopPropagation();
                },
                onClick: (x, y, event) => {
                    event.stopPropagation();
                },
                onWheel: (delta, event) => {
                    event.stopPropagation();
                }
            },
            mhContent = new MouseListener(markerRef.content, inputHandlerContent),
            thContent = new TouchListener(markerRef.content, inputHandlerContent);

        markerRef.listenersContent.push(mhClose, thClose, mhParent, thParent, mhEdit, thEdit, mhContent, thContent);
        markerRef.listenersContent.forEach((l) => {
            l.enable();
        });
    }

    /**
     * update existing marker
     *
     * @param {object} self instance reference
     * @param {object} marker marker data
     */
    function updateMarker(self, marker) {
        const markerRef = self.markerElements[marker.type][marker.id];
        if (markerRef) {
            markerRef.marker = marker;
            if (self.currentMarker && self.currentMarker.ref === markerRef) {
                self.currentMarker.data = marker;
            }
        }
    }

    /**
     * create new marker element
     *
     * @param {object} self instance reference
     * @param {object} marker marker data
     */
    function createMarker(self, marker) {
        const markerRef = self.markerElements[marker.type][marker.id] = {
            marker: marker,
            element: getMarkerTemplate(self, marker.type).cloneNode(true),
            icon: null,
            listenersIcon: [],
            listenersContent: []
        };

        if (
            [
                lookupMarkerElement(markerRef, "icon", "marker-icon"),
                lookupMarkerElement(markerRef, "title", "marker-title"),
                lookupMarkerElement(markerRef, "text", "marker-text"),
                lookupMarkerElement(markerRef, "time", "marker-time"),
                lookupMarkerElement(markerRef, "user", "marker-user"),
                lookupMarkerElement(markerRef, "info", "marker-info"),
                lookupMarkerElement(markerRef, "close", "marker-close"),
                lookupMarkerElement(markerRef, "content", "marker-content"),
                lookupMarkerElement(markerRef, "edit", "marker-edit")
            ].every((value) => value)
        ) {
            const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                inputHandler = {
                    onPress: () => { /* implementation of handler stops event bubbling */ },
                    onRelease: () => { /* implementation of handler stops event bubbling */ },
                    onClick: async () => {
                        if (!self.trendWidget.isLocked() && !(self.currentMarker && self.currentMarker.ref === markerRef)) {
                            await onMarkerOpen(self, markerRef);
                        }
                    }
                },
                mh = new MouseListener(markerRef.icon, inputHandler),
                th = new TouchListener(markerRef.icon, inputHandler);
            markerRef.listenersIcon.push(mh, th);
            markerRef.listenersIcon.forEach((l) => {
                l.enable();
            });
        }
    }

    module.MarkerLayer = class MarkerLayer {
        /**
         * Creates an instance of MarkerLayer.
         *
         * @param {HTMLElement} element parent element to attach marker-layer to
         * @param {object} trendWidget trend instance reference
         */
        constructor(element, trendWidget) {
            this.parent = element;
            this.trendWidget = trendWidget;
            this.width = 1;
            this.scale = 1;
            this.min = 0;
            this.max = 60000;
            this.markers = [];
            this.markerElements = {
                alarm: {},
                comment: {}
            };
            this.currentMarker = null;
            this.mayModifyThreads = null;
            updateSize(this, this.parent.getBoundingClientRect());
        }
        /**
         * draw marker layer
         *
         */
        draw() {
            const activeMarkers = [];
            this.markers.forEach((marker) => {
                if (this.markerElements[marker.type]) {
                    if (!this.markerElements[marker.type][marker.id]) {
                        createMarker(this, marker);
                    } else {
                        updateMarker(this, marker);
                    }
                    const markerRef = this.markerElements[marker.type][marker.id];
                    const xCoordinate = getXCoordinate(this, marker.timestamp);
                    if (marker.timestamp < this.min || marker.timestamp > this.max) {
                        markerRef.element.style.display = "none";
                    } else {
                        markerRef.element.style.display = "";
                    }
                    markerRef.element.style.left = `${xCoordinate}px`;
                    if (!markerRef.element.parentNode) {
                        this.parent.appendChild(markerRef.element);
                    }
                    activeMarkers.push(markerRef.marker);
                }
            });

            Object.keys(this.markerElements).forEach((markerType) => {
                Object.keys(this.markerElements[markerType]).forEach((markerId) => {
                    const markerRef = this.markerElements[markerType][markerId];
                    if (!activeMarkers.includes(markerRef.marker)) {
                        if (this.currentMarker && markerRef === this.currentMarker.ref) {
                            onMarkerClose(this, markerRef);
                        }
                        markerRef.listenersIcon.forEach((l) => {
                            l.disable();
                        });
                        markerRef.listenersIcon = [];
                        this.parent.removeChild(markerRef.element);
                        delete this.markerElements[markerType][markerId];
                    }
                });
            });
        }
        /**
         * add markers to marker-layer
         *
         * @param {object[]} markers marker data
         */
        addMarkers(markers) {
            markers.forEach((marker) => {
                this.markers.push(marker);
            });
            this.markers.sort((a, b) => a.timestamp - b.timestamp);
            this.draw();
        }
        /**
         * clear markers from marker-layer
         *
         * @param {string|null} [markerType=null] marker type or `null` to clear all
         */
        clearMarkers(markerType = null) {
            this.markers = (typeof markerType === "string") ? this.markers.filter((marker) => marker.type !== markerType) : [];
        }
        /**
         * set marker-layer options
         *
         * @param {object} options marker-layer options
         */
        setOptions(options) {
            if (options.x) {
                if (typeof options.x.min === "number") {
                    this.min = options.x.min;
                }
                if (typeof options.x.max === "number") {
                    this.max = options.x.max;
                }
                this.scale = this.width / (this.max - this.min);
                this.draw();
            }
        }
        /**
         * trigger maker-layer size update
         *
         */
        updateSize() {
            updateSize(this, this.parent.getBoundingClientRect());
            this.draw();
            if (this.currentMarker) {
                updateMarkerSize(this, this.currentMarker.ref);
            }
        }
    };
}());

/**
 * Module to implement value scales for iq-trend.
 *
 * @module visuals/controls/iqtrend/scale
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.scale",
        /** @lends module:visuals/controls/iqtrend/scale */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    /**
     * initial value for height of labeled steps
     * @type {number}
     */
    const stepSize = 30;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
    }

    function updateStroke(self) {
        const style = getComputedStyle(self.parent);
        self.strokeStyle = style.getPropertyValue("--scale-color");
    }

    /**
     * step widths to use for label generation
     * @type {number[]}
     */
    const stepWidths = [
        1,
        0.5,
        0.25,
        0.2
    ];

    /**
     * change number of maximum decimal digits
     *
     * @param {number} value value
     * @param {number} digits number of decimal digits
     * @return {number} output value
     */
    function toDigits(value, digits) {
        return parseFloat(value.toFixed(digits));
    }

    /**
     * get step size for scale labels
     *
     * @param {number} min minimum value
     * @param {number} max maximum value
     * @param {number} maxNumSteps maximum number of steps
     * @return {number} step size
     */
    function getSimpleStep(min, max, maxNumSteps) {
        const range = max - min;
        let order = Math.ceil(Math.log10(range));

        let step = 1;
        let numSteps = 0;
        while (numSteps < maxNumSteps) {
            for (let i = 0; i < stepWidths.length; i++) {
                let testStep = stepWidths[i] * (10 ** order);
                numSteps = range / testStep;
                if (numSteps <= maxNumSteps) {
                    step = testStep;
                } else {
                    break;
                }
            }
            order -= 1;
        }

        return step;
    }

    /**
     * get scale range information
     *
     * @param {number} minY minimum y-value
     * @param {number} maxY maximum y-value
     * @param {number} maxNumSteps maximum number of scale steps
     * @return {object} scale range information
     */
    function getRangeInfo(minY, maxY, maxNumSteps) {
        const step = getSimpleStep(minY, maxY, maxNumSteps),
            digits = getDigits(step),
            startY = Math.floor(minY / step) * step,
            endY = startY + Math.ceil((maxY - startY) / step) * step;

        return {
            step: toDigits(step, digits),
            start: toDigits(startY, digits),
            range: toDigits(endY - startY, digits)
        };
    }

    /**
     * get decimal digits of value
     *
     * @param {number} value input value
     * @return {number} number of decimal digits
     */
    function getDigits(value) {
        value = parseFloat(value.toFixed(14));
        const valueString = String(value);
        let valueDigits = valueString.indexOf(".");
        if (valueDigits !== -1) {
            valueDigits = valueString.length - valueDigits - 1;
        } else {
            valueDigits = 0;
        }
        return valueDigits;
    }

    /**
     * check if supplied unit is a unit-class
     *
     * @param {string|number} unitText input unit
     * @return {boolean} `true` if unit is unit-class, `false` else
     */
    function isUnitClass(unitText) {
        return String(parseInt(unitText)) === String(unitText);
    }

    /**
     * get stroke style for value
     *
     * @param {object} self instance reference
     * @param {string} valueName name of value
     * @return {object|null} stroke style info or `null` if none found
     */
    function getValueStroke(self, valueName) {
        let dataset = self.dataset;

        if (valueName !== self.valueName) {
            dataset = self.dataset.trendWidget.vars.datasets.find((ds) => ds.name === valueName);
        }

        return dataset ? dataset.graph.style.value.strokeStyle : null;
    }

    /**
     * add marker for line associated with scale
     *
     * @param {object} self instance reference
     * @param {string} valueName name of value
     */
    function addLineMarker(self, valueName) {
        const lineMarker = document.createElement("div");

        lineMarker.classList.add("iqel-line-marker");
        lineMarker.style.setProperty("--line-color", getValueStroke(self, valueName));
        self.lineMarkers.push(lineMarker);
    }

    /**
     * update unit information
     *
     * @param {object} self instance reference
     */
    function updateUnitInfo(self) {
        const valueSettings = self.dataset.trendWidget.vars.datasource.source.getValueSettings(),
            uc = shmi.requires("visuals.tools.unitClasses"),
            identicalUnits = self.additionalValues.length === 0 || (valueSettings[self.valueName] && self.additionalValues.every((valueName) => valueSettings[valueName] && valueSettings[self.valueName].unit === valueSettings[valueName].unit));
        let unitText = "";

        if (identicalUnits && valueSettings[self.valueName]) {
            if (isUnitClass(valueSettings[self.valueName].unit)) {
                const ucAdapter = uc.getSelectedAdapter(valueSettings[self.valueName].unit);
                if (ucAdapter) {
                    unitText = ucAdapter.unitText;
                } else {
                    unitText = valueSettings[self.valueName].unit;
                }
            } else {
                unitText = valueSettings[self.valueName].unit === null ? "" : valueSettings[self.valueName].unit;
            }
            self.unit = valueSettings[self.valueName].unit;
        }

        self.unitInfo.textContent = shmi.localize(unitText);
    }

    /**
     * update scale info (line-markers, unit-info) & dimensions
     *
     * @param {object} self instance reference
     */
    function updateScaleInfo(self) {
        self.lineMarkers.forEach((lineMarker) => {
            lineMarker.parentNode.removeChild(lineMarker);
        });
        self.lineMarkers = [];

        addLineMarker(self, self.valueName);
        self.additionalValues.forEach((valueName) => {
            addLineMarker(self, valueName);
        });

        self.lineMarkers.forEach((lineMarker) => {
            self.markerContainer.appendChild(lineMarker);
        });

        updateUnitInfo(self);

        const parentRect = self.parent.getBoundingClientRect();
        const infoRect = self.scaleInfo.getBoundingClientRect();
        self.scaleInfoOffset = (infoRect.top - parentRect.top) + self.scaleInfo.offsetHeight;
    }

    /**
     * get scale canvas coordinate for specified value
     *
     * @param {object} self instance reference
     * @param {number} scale scaling factor
     * @param {number} value input value
     * @return {number} canvas y-coordinate
     */
    function getCoordinate(self, scale, value) {
        return self.height - (value - self.min) * scale + 1;
    }

    /**
     * draw scale marker
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value input value
     * @param {number} width marker width
     * @return {number} canvas y-coordinate
     */
    function drawScaleMarker(self, scale, value, width) {
        const y = getCoordinate(self, scale, value),
            widthOffset = self.lineWidth / 2;

        if (self.mirrored) {
            self.context.moveTo(1, y - widthOffset);
            self.context.lineTo(width + 1, y - widthOffset);
        } else {
            self.context.moveTo(self.width + 1, y - widthOffset);
            self.context.lineTo(self.width - width + 1, y - widthOffset);
        }

        return y;
    }

    /**
     * draw small markers between labeled markers
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {object} stepInfo stepping info
     * @param {number} stepDistance step distance
     * @param {number} markerValue value at marker
     * @param {number} index marker index
     */
    function drawSmallMarkers(self, scale, stepInfo, stepDistance, markerValue, index) {
        if (stepDistance > 25) {
            if (index === 0) { //draw markers before first labeled marker if in visible area
                const smallMarkerValue = markerValue - stepInfo.step / 2;
                if (smallMarkerValue >= self.min) {
                    drawScaleMarker(self, scale, smallMarkerValue, self.width - 2);
                }
            }

            {
                const smallMarkerValue = markerValue + stepInfo.step / 2;
                drawScaleMarker(self, scale, smallMarkerValue, self.width - 2);
            }
        }
    }

    /**
     * Creates a listener for the `window.resize` event.
     *
     * @param {function} handlerFunc Event handler
     * @returns {object}
     */
    function makeResizeListener(handlerFunc) {
        return {
            disable: () => window.removeEventListener("resize", handlerFunc),
            enable: () => window.addEventListener("resize", handlerFunc)
        };
    }

    module.Scale = class Scale {
        /**
         * Creates an instance of Scale.
         * @param {HTMLElement} element parent element to attach scale to
         * @param {object} {
         *             min = 0,
         *             max = 100,
         *             numSteps = 21,
         *             mirrored = false,
         *             location = "left",
         *             align = "left",
         *             strokeStyle = "rgb(0, 0, 0)",
         *             lineWidth = 1,
         *             hidden = false,
         *             valueName = "value",
         *             additionalValues = []
         *         } scale options
         * @param {object} dataset parent dataset of scale
         */
        constructor(element, {
            min = 0,
            max = 100,
            numSteps = 21,
            mirrored = false,
            location = "left",
            align = "left",
            lineWidth = 1,
            hidden = false,
            valueName = "value",
            additionalValues = [],
            autoWidth = true
        }, dataset) {
            this.dataset = dataset;
            this.canvas = document.createElement("canvas");
            this.wrapper = document.createElement("div");
            this.wrapper.classList.add("iqel-scale-wrapper");
            this.parent = element;
            this.canvas.classList.add("iqel-scale-canvas");
            this.scaleInfo = document.createElement("div");
            this.scaleInfo.classList.add("iqel-scale-info");
            this.markerContainer = document.createElement("div");
            this.markerContainer.classList.add("iqel-line-markers");
            this.lineMarkers = [];
            this.valueName = valueName;
            this.additionalValues = additionalValues;
            this.unitInfo = document.createElement("div");
            this.unitInfo.classList.add("iqel-unit-info");
            this.scaleInfo.appendChild(this.markerContainer);
            this.scaleInfo.appendChild(this.unitInfo);
            this.wrapper.appendChild(this.scaleInfo);

            let rafId = 0;
            this.resizeListener = makeResizeListener(() => {
                shmi.caf(rafId);
                rafId = shmi.raf(() => {
                    if (this.parent) {
                        this.width = this.canvas.offsetWidth - 2;
                        updateSize(this);
                        updateStroke(this);
                        updateScaleInfo(this);
                        this.draw();
                    }
                });
            });

            this.unit = null;

            this.mirrored = mirrored;
            this.location = location;
            if (location === "left" && mirrored) {
                this.wrapper.classList.add("mirrored");
            } else if (location === "right" && !mirrored) {
                this.wrapper.classList.add("mirrored");
            }
            this.lineWidth = lineWidth;
            this.hidden = hidden;
            if (this.hidden) {
                this.wrapper.classList.add("hidden");
            }

            this.markers = [];
            for (let i = 0; i < numSteps + 1; i++) {
                const marker = document.createElement("div");
                marker.classList.add("iqel-scale-label");
                marker.style.display = "none";
                this.wrapper.appendChild(marker);
                this.markers.push(marker);
            }

            this.wrapper.appendChild(this.canvas);
            this.parent.appendChild(this.wrapper);

            const rect = this.parent.getBoundingClientRect();
            this.width = this.canvas.offsetWidth - 2;
            this.height = rect.height - 2;

            const { getFontStyle } = shmi.requires("visuals.controls.iqtrend.common");
            this.fontStyle = getFontStyle(this.markers[0]);
            const { getTextDimensions } = shmi.requires("visuals.controls.iqtrend.common");
            const markerDims = getTextDimensions("123", this.fontStyle);
            this.markerHeight = markerDims.textHeight;

            this.min = min;
            this.max = max;
            this.numSteps = numSteps;
            this.autoWidth = autoWidth;
            this.baseWidth = 44;
            try {
                this.baseWidth = parseInt(getComputedStyle(this.parent).getPropertyValue("--scale-width"));
            } catch (exc) {
                console.error("Error computing base label width (possibly required definition of --scale-width missing):", exc);
            }
            this.currentWidth = this.baseWidth;

            updateScaleInfo(this);
            updateSize(this);
            updateStroke(this);

            this.context = this.canvas.getContext("2d");
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = this.canvas.offsetWidth - 2;
                this.height = entry.contentRect.height - 2;
                updateSize(this);
                updateStroke(this);
                updateScaleInfo(this);
                this.draw();
            });

            this.observer.observe(this.parent);
            this.themeToken = shmi.listen("webiq-theme-settings", () => {
                updateStroke(this);
                this.draw();
            });

            if (this.autoWidth) {
                this.labelObserver = new ResizeObserver(() => {
                    let newWidth = this.baseWidth;
                    this.markers.forEach((m) => {
                        const markerRect = m.getBoundingClientRect(),
                            calculatedWidth = markerRect.width + this.width + 4;
                        if (calculatedWidth > newWidth) {
                            const rectWidth = this.baseWidth + (10 * Math.ceil((calculatedWidth - this.baseWidth) / 10));
                            if (newWidth < rectWidth) {
                                newWidth = rectWidth;
                            }
                        }
                    });
                    if ((newWidth > this.baseWidth) && (this.currentWidth !== newWidth)) {
                        this.currentWidth = newWidth;
                        this.wrapper.style.setProperty("--scale-width", `${this.currentWidth}px`);
                    } else if (newWidth <= this.baseWidth) {
                        this.wrapper.style.removeProperty("--scale-width");
                        this.currentWidth = this.baseWidth;
                    }
                });
                this.markers.forEach((m) => {
                    this.labelObserver.observe(m);
                });
            }

            this.resizeListener.enable();
            this.draw();
        }
        /**
         * set scale options
         *
         * @param {object} options scale options
         */
        setOptions(options) {
            let redraw = false,
                updateInfo = false;

            if (this.parent === null) {
                return;
            }

            if (options.y) {
                if (typeof options.y.min === "number") {
                    this.min = options.y.min;
                }
                if (typeof options.y.max === "number") {
                    this.max = options.y.max;
                }
                redraw = true;
            }

            if (typeof options.location === "string") {
                if (options.location !== this.location) {
                    if (options.location === "left") {
                        this.location = "left";
                        this.mirrored = !this.mirrored;
                        this.parent = this.dataset.trendWidget.vars.elements.scalesLeft;
                        this.parent.appendChild(this.wrapper);
                    } else if (options.location === "right") {
                        this.location = "right";
                        this.mirrored = !this.mirrored;
                        this.parent = this.dataset.trendWidget.vars.elements.scalesRight;
                        this.parent.appendChild(this.wrapper);
                    }

                    if (this.location === "left" && this.mirrored) {
                        this.wrapper.classList.add("mirrored");
                    } else if (this.location === "right" && !this.mirrored) {
                        this.wrapper.classList.add("mirrored");
                    } else {
                        this.wrapper.classList.remove("mirrored");
                    }
                    redraw = true;
                }
            }

            if (typeof options.hidden === "boolean") {
                if (this.hidden && !options.hidden) {
                    updateInfo = true;
                }
                this.hidden = options.hidden;
                if (this.hidden) {
                    this.wrapper.classList.add("hidden");
                } else {
                    this.wrapper.classList.remove("hidden");
                    this.width = this.canvas.offsetWidth;
                    updateSize(this);
                }
                redraw = true;
            }

            if (typeof options.addValue === "string") {
                if (!this.additionalValues.includes(options.addValue) && options.addValue !== this.valueName) {
                    this.additionalValues.push(options.addValue);
                    updateInfo = true;
                }
            }

            if (typeof options.removeValue === "string") {
                if (this.additionalValues.includes(options.removeValue)) {
                    this.additionalValues.splice(this.additionalValues.indexOf(options.removeValue), 1);
                    updateInfo = true;
                    redraw = true;
                }
            }

            if (options.style) {
                updateInfo = true;
            }

            if (typeof options.unit !== "undefined" && this.unit !== options.unit) {
                updateInfo = true;
            }

            if (updateInfo) {
                updateScaleInfo(this);
            }

            if (redraw) {
                this.draw();
            }
        }
        /**
         * draw scale
         *
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            this.context.lineWidth = this.lineWidth;
            this.context.strokeStyle = this.strokeStyle;
            const widthOffset = this.lineWidth / 2;

            this.context.beginPath();
            if (this.mirrored) {
                this.context.moveTo(1 + widthOffset, 1);
                this.context.lineTo(1 + widthOffset, this.height + 1);
            } else {
                this.context.moveTo(this.width + 1 - widthOffset, 1);
                this.context.lineTo(this.width + 1 - widthOffset, this.height + 1);
            }

            const targetSteps = Math.max(1, Math.min(this.numSteps, Math.floor(this.height / stepSize)));

            const stepInfo = getRangeInfo(this.min, this.max, targetSteps),
                stepDigits = getDigits(stepInfo.step),
                numMarkers = toDigits(stepInfo.range / stepInfo.step, stepDigits) + 1,
                scale = this.height / (this.max - this.min),
                stepDistance = stepInfo.step * scale;

            const markerLog = [];
            for (let i = 0; i < numMarkers && i < this.markers.length; i++) {
                const markerValue = toDigits(stepInfo.start + (i * stepInfo.step), stepDigits);
                if (this.min <= markerValue && markerValue <= this.max) {
                    const y = drawScaleMarker(this, scale, markerValue, this.width);

                    drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);

                    if (y > this.scaleInfoOffset + this.markerHeight) {
                        this.markers[i].textContent = toDigits(markerValue, stepDigits);
                        this.markers[i].style.top = `${y - 1}px`;
                        this.markers[i].style.display = "block";
                    } else {
                        this.markers[i].style.display = "none";
                    }
                } else {
                    if (i === 0) {
                        drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);
                    }
                    this.markers[i].style.display = "none";
                }
            }
            this.markers.slice(numMarkers, this.markers.length).forEach((marker) => {
                marker.style.display = "none";
            });
            this.context.stroke();
            log(`Scale Markers: ${markerLog.join(", ")}`);
        }
        /**
         * remove scale
         *
         */
        remove() {
            if (this.parent) {
                this.observer.disconnect();
                if (this.labelObserver) {
                    this.labelObserver.disconnect();
                }
                this.parent.removeChild(this.wrapper);
                this.parent = null;
            }

            if (this.themeToken) {
                this.themeToken.unlisten();
                this.themeToken = null;
            }

            this.resizeListener.disable();
        }
    };
}());

/**
 * Module to implement scale-group to associate multiple values with single scale
 *
 * @module visuals/controls/iqtrend/scalegroup
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.scalegroup",
        /** @lends module:visuals/controls/iqtrend/scalegroup */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update limits of scale-group
     *
     * @param {object} self instance reference
     */
    function updateLimits(self) {
        const { min, max } = self.getCurrentLimits();

        Object.keys(self.values).forEach((name) => {
            self.values[name].dataset.graph.setOptions({
                y: {
                    min,
                    max
                }
            });
            self.values[name].dataset.scale.setOptions({
                y: {
                    min,
                    max
                }
            });
        });
    }

    /**
     * set limit options for member scale
     *
     * @param {object} self instance reference
     * @param {string} name name of value
     * @param {object} options limit options
     */
    function setLimitOption(self, name, options) {
        if (!self.limits[name]) {
            self.limits[name] = {
                min: null,
                max: null
            };
        }
        if (typeof options.y.min === "number") {
            self.limits[name].min = options.y.min;
        }
        if (typeof options.y.max === "number") {
            self.limits[name].max = options.y.max;
        }
    }

    module.ScaleGroup = class ScaleGroup {
        /**
         * Creates an instance of ScaleGroup.
         * @param {number} groupId scale-group ID
         * @param {object} trendWidget iq-trend reference
         * @param {object} [initialDataset=null] initial dataset reference
         */
        constructor(groupId, trendWidget, initialDataset = null) {
            this.groupId = groupId;
            this.trendWidget = trendWidget;
            this.values = {};
            if (initialDataset) {
                this.values[initialDataset.name] = {
                    name: initialDataset.name,
                    dataset: initialDataset
                };
            }
            this.master = initialDataset ? this.values[initialDataset.name] : null;
            this.limits = {};
        }
        /**
         * add value dataset to scale-group
         *
         * @param {string} name name of value
         * @param {object} dataset dataset reference
         */
        addValue(name, dataset) {
            if (!this.values[name]) {
                this.values[name] = {
                    name,
                    dataset
                };
                if (!this.master) {
                    this.master = this.values[name];
                }
                this.master.dataset.scale.setOptions({
                    addValue: name
                });
                if (dataset !== this.master.dataset && dataset.scale) {
                    dataset.scale.setOptions({
                        hidden: true
                    });
                }
            }
        }
        /**
         * remove value from scale-group
         *
         * @param {string} name name of value
         */
        removeValue(name) {
            if (this.values[name]) {
                const wasMaster = this.values[name] === this.master;
                delete this.values[name];
                delete this.limits[name];
                if (wasMaster) {
                    this.master.dataset.scale.additionalValues.slice().forEach((addValue) => {
                        this.master.dataset.scale.setOptions({
                            removeValue: addValue
                        });
                    });
                    const newMasterName = this.getValueNames()[0];
                    if (newMasterName) {
                        //update master reference
                        this.master = this.values[newMasterName];
                        Object.keys(this.values).forEach((valueName) => {
                            if (valueName !== this.master.name) {
                                this.master.dataset.scale.setOptions({
                                    addValue: valueName
                                });
                            }
                        });
                        this.master.dataset.scale.setOptions({
                            hidden: false
                        });
                    } else {
                        this.master = null;
                    }
                } else if (this.master) {
                    this.master.dataset.scale.setOptions({
                        removeValue: name
                    });
                }
                updateLimits(this);
            }
        }
        /**
         * get value limits
         *
         * @return {object} value limits (min, max)
         */
        getValueLimits() {
            const valueSettings = this.trendWidget.vars.datasource.source.getValueSettings();
            let min = null,
                max = null;

            this.getValueNames().forEach((name) => {
                const vs = valueSettings[name];
                if (vs && typeof vs.min === "number") {
                    if (min === null || min > vs.min) {
                        min = vs.min;
                    }
                }

                if (vs && typeof vs.max === "number") {
                    if (max === null || max < vs.max) {
                        max = vs.max;
                    }
                }
            });

            return {
                min,
                max
            };
        }
        /**
         * get current scale-group limits (including zoom)
         *
         * @return {object} scale-group limits (min, max)
         */
        getCurrentLimits() {
            let min = null,
                max = null;

            Object.keys(this.limits).forEach((name) => {
                if (min === null || this.limits[name].min < min) {
                    min = this.limits[name].min;
                }
                if (max === null || this.limits[name].max > max) {
                    max = this.limits[name].max;
                }
            });

            return {
                min,
                max
            };
        }
        /**
         * get value names of scale-group
         *
         * @return {string[]} value names
         */
        getValueNames() {
            return Object.keys(this.values);
        }

        /**
         * set scale-group options
         *
         * @param {object} options scale options
         * @param {string|null} name name of value or `null` to target all
         */
        setOptions(options, name) {
            if (options.y) {
                if (name === null) {
                    this.getValueNames().forEach((valueName) => {
                        setLimitOption(this, valueName, options);
                    });
                } else {
                    setLimitOption(this, name, options);
                }
                updateLimits(this);
            }
        }
    };
}());

/**
 * Module to implement task-queue for trend data operations
 *
 * @module visuals/controls/iqtrend/taskqueue
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.taskqueue",
        /** @lends module:visuals/controls/iqtrend/taskqueue */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const states = shmi.requires("visuals.controls.iqtrend.common").taskStates;

    /**
     * remove task that are either completed or canceled from queue
     *
     * @param {object} self instance reference
     */
    function cleanupTasks(self) {
        self._tasks = self._tasks.filter((task) => task.state === states.CREATED || task.state === states.RUNNING);
    }

    /**
     * check queue for tasks and execute next available task
     *
     * @param {object} self instance reference
     * @return {Promise} promise resolving when task is completed
     */
    function checkTasks(self) {
        cleanupTasks(self);
        notifyWatchers(self);

        const nextTask = self._tasks.find((task) => task.state === states.CREATED);

        return nextTask ? nextTask.run().then((result) => checkTasks(self)) : Promise.resolve;
    }

    /**
     * notify watchers of task queue with updated state
     *
     * @param {object} self instance reference
     */
    function notifyWatchers(self) {
        //..update watchers with current state
        self._watchers.forEach((watcherCallback) => {
            watcherCallback({
                source: self,
                loading: self.loading
            });
        });
    }

    module.TaskQueue = class TaskQueue {
        constructor() {
            this._tasks = [];
            this._watchers = [];
        }
        /**
         * get current loading / busy state
         *
         * @readonly
         * @type {boolean} `true` if currently loading, `false` else
         */
        get loading() {
            return !!this._tasks.length;
        }
        /**
         * get queued tasks
         *
         * @return {object[]} tasks in queue
         */
        getTasks() {
            return this._tasks;
        }
        /**
         * queue task for execution
         *
         * @param {object} task task reference
         * @return {Promise} promise resolving when task has been queued
         */
        queueTask(task) {
            cleanupTasks(this);
            this._tasks.push(task);
            if (this._tasks.length === 1) {
                notifyWatchers(this);
                return this._tasks[0].run().then(() => checkTasks(this));
            }
            return Promise.resolve();
        }
        /**
         * watch state of task queue
         *
         * @param {function} callback notification callback for state updates
         * @return {object} reference to stop watching
         */
        watch(callback) {
            this._watchers.push(callback);
            return {
                unwatch: () => {
                    const idx = this._watchers.indexOf(callback);
                    if (idx !== -1) {
                        this._watchers.splice(idx, 1);
                    }
                }
            };
        }
    };
}());

/**
 * Module to implement timeline (x-axis) for iq-trend
 *
 * @module visuals/controls/iqtrend/timeline
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.timeline",
        /** @lends module:visuals/controls/iqtrend/timeline */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
    }

    function updateStroke(self) {
        const style = getComputedStyle(self.parent);
        self.strokeStyle = style.getPropertyValue("--timeline-color");
    }

    /**
     * time unit definition
     * @type {object}
     */
    const time = {
        SECOND: 1000,
        MINUTE: 60000,
        HOUR: 3600000,
        DAY: 86400000,
        WEEK: 604800000,
        MONTH: 2592000000,
        YEAR: 31536000000
    };

    /**
     * default marker label definition
     * @type {object[]}
     */
    const defaultMarkerFormats = [
        {
            unit: time.YEAR,
            format: "$YYYY"
        },
        {
            unit: time.MONTH,
            format: "$DD.$MM"
        },
        {
            unit: time.WEEK,
            format: "$DD.$MM"
        },
        {
            unit: time.DAY,
            format: "$DD.$MM"
        },
        {
            unit: time.HOUR,
            format: "$HH:$mm"
        },
        {
            unit: time.MINUTE,
            format: "$HH:$mm"
        },
        {
            unit: time.SECOND,
            format: "$mm:$ss"
        },
        {
            unit: 1,
            format: "$ss.$SSS"
        }
    ];

    const defaultStartMarkerFormat = {
        time: "$HH:$mm:$ss",
        date: "$DD.$MM.$YYYY"
    };

    /**
     * get marker label format for specified step size
     *
     * @param {number} step step size
     * @param {object[]} formats marker formats
     * @return {object} marker format for requested step size
     */
    function getMarkerFormat(step, formats) {
        return formats.find((format, idx) => step % format.unit === 0 || idx === (formats.length - 1));
    }

    /**
     * get marker text for specified value
     *
     * @param {object} self instance reference
     * @param {object} stepInfo stepping info
     * @param {number} value value (timestamp)
     * @return {string} marker text
     */
    function getMarkerText(self, stepInfo, value) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            markerFormat = getMarkerFormat(stepInfo.step, self.markerFormats);
        return formatDateTime(value / 1000, markerFormat ? markerFormat.format : undefined);
    }

    /**
     * available step widths to choose from
     * @type {number[]}
     */
    const stepWidths = [
        1,
        5,
        10,
        25,
        50,
        100,
        250,
        500,
        time.SECOND,
        2 * time.SECOND,
        5 * time.SECOND,
        10 * time.SECOND,
        15 * time.SECOND,
        30 * time.SECOND,
        time.MINUTE,
        2 * time.MINUTE,
        5 * time.MINUTE,
        10 * time.MINUTE,
        15 * time.MINUTE,
        20 * time.MINUTE,
        30 * time.MINUTE,
        time.HOUR,
        2 * time.HOUR,
        3 * time.HOUR,
        6 * time.HOUR,
        12 * time.HOUR,
        time.DAY,
        2 * time.DAY,
        time.WEEK,
        2 * time.WEEK,
        time.MONTH,
        2 * time.MONTH,
        3 * time.MONTH,
        6 * time.MONTH,
        time.YEAR
    ];

    /**
     * get step size for specified range
     *
     * @param {number} min minimum timestamp
     * @param {number} max maximum timestamp
     * @param {number} maxNumSteps maximum number of steps
     * @return {number} step size
     */
    function getTimeStep(min, max, maxNumSteps) {
        const range = max - min;

        let step = 1;
        let numSteps = 0;
        const timeSteps = stepWidths.slice().reverse();
        for (let i = 0; i < timeSteps.length; i++) {
            const testStep = timeSteps[i];
            numSteps = range / testStep;
            if (numSteps <= maxNumSteps) {
                step = testStep;
            } else {
                break;
            }
        }

        return step;
    }

    /**
     * get range information for specified time range
     *
     * @param {number} min minimum timestamp
     * @param {number} max maximum timestamp
     * @param {number} maxNumSteps maximum number of steps
     * @return {object} range info
     */
    function getRangeInfo(min, max, maxNumSteps) {
        const step = getTimeStep(min, max, maxNumSteps),
            start = Math.floor(min / step) * step,
            endY = start + Math.ceil((max - start) / step) * step;

        return {
            step,
            start,
            range: endY - start
        };
    }

    /**
     * get x-coordinate for specified timestamp
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value timestamp in ms
     * @return {number} x-coordinate on canvas
     */
    function getCoordinate(self, scale, value) {
        return (value - self.min) * scale + 1;
    }

    /**
     * draw scale marker
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value timestamp in ms
     * @param {number} width marker width
     * @return {number} x-coordinate on canvas
     */
    function drawScaleMarker(self, scale, value, width) {
        const x = getCoordinate(self, scale, value);

        if (self.mirrored) {
            self.context.moveTo(x, self.height + 1);
            self.context.lineTo(x, (width) + 1);
        } else {
            self.context.moveTo(x, 1 + width);
            self.context.lineTo(x, 1);
        }

        return x;
    }

    /**
     * draw small markers between labeled markers
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {object} stepInfo stepping info
     * @param {number} stepDistance step distance
     * @param {number} markerValue value at marker
     * @param {number} index marker index
     */
    function drawSmallMarkers(self, scale, stepInfo, stepDistance, markerValue, index) {
        if (stepDistance > 100) {
            const smallDiff = stepInfo.step / 4;
            if (index === 0) {
                for (let j=0; j<3; j++) {
                    const smallMarkerValue = markerValue - (j + 1) * smallDiff;
                    if (smallMarkerValue >= self.min) {
                        drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                    }
                }
            }
            for (let j=0; j<3; j++) {
                const smallMarkerValue = markerValue + (j + 1) * smallDiff;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        } else if (stepDistance > 75) {
            if (index === 0) {
                const smallMarkerValue = markerValue - stepInfo.step / 2;
                if (smallMarkerValue >= self.min) {
                    drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                }
            }

            {
                const smallMarkerValue = markerValue + stepInfo.step / 2;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        }
    }

    /**
     * update start marker texts
     *
     * @param {object} self instance reference
     */
    function updateStartMarker(self) {
        const { formatDateTime } = shmi.requires("visuals.tools.date");

        self.startDate.textContent = formatDateTime(self.min / 1000, self.startMarkerFormat.date);
        self.startTime.textContent = formatDateTime(self.min / 1000, self.startMarkerFormat.time);
    }

    /**
     * Creates a listener for the `window.resize` event.
     *
     * @param {function} handlerFunc Event handler
     * @returns {object}
     */
    function makeResizeListener(handlerFunc) {
        return {
            disable: () => window.removeEventListener("resize", handlerFunc),
            enable: () => window.addEventListener("resize", handlerFunc)
        };
    }

    module.Timeline = class Timeline {
        /**
         * Creates an instance of Timeline.
         * @param {HTMLElement} element parent element to attach timeline to
         * @param {object} {
         *             min = 0,
         *             max = 100,
         *             numSteps = 21,
         *             mirrored = false,
         *             strokeStyle = "rgb(0, 0, 0)",
         *             markerFormats,
         *             startMarkerFormat = {
         *                 time: "$HH:$mm:$ss",
         *                 date: "$DD.$MM.$YYYY"
         *             }
         *         } timeline options
         */
        constructor(element, {
            min = 0,
            max = 100,
            numSteps = 21,
            mirrored = false,
            markerFormats,
            startMarkerFormat
        }, updateCallback) {
            this.canvas = document.createElement("canvas");
            this.parent = element;
            this.canvas.classList.add("iqel-timeline-canvas");
            this.updateCallback = typeof updateCallback === "function" ? updateCallback : () => {};

            this.mirrored = mirrored;
            this.markerFormats = Array.isArray(markerFormats) ? markerFormats : defaultMarkerFormats;
            this.markerFormats.forEach((markerFormat) => {
                markerFormat.format = shmi.localize(markerFormat.format);
            });
            this.startMarkerFormat = startMarkerFormat && typeof startMarkerFormat.time === "string" && typeof startMarkerFormat.date === "string" ? startMarkerFormat : defaultStartMarkerFormat;
            this.startMarkerFormat.date = shmi.localize(this.startMarkerFormat.date);
            this.startMarkerFormat.time = shmi.localize(this.startMarkerFormat.time);

            let rafId = 0;
            this.resizeListener = makeResizeListener(() => {
                shmi.caf(rafId);
                rafId = shmi.raf(() => {
                    if (this.parent) {
                        this.height = this.canvas.offsetHeight - 2;
                        updateSize(this);
                        updateStroke(this);
                        this.draw();
                    }
                });
            });

            let tmpMaxFormat = "";
            this.markerFormats.forEach((format) => {
                if (format.format.length > tmpMaxFormat.length) {
                    tmpMaxFormat = format.format;
                }
            });
            this.maxMarkerFormat = tmpMaxFormat;

            this.markers = [];
            for (let i = 0; i < numSteps + 1; i++) {
                const marker = document.createElement("div");
                marker.classList.add("iqel-timeline-label");
                marker.style.display = "none";
                this.parent.appendChild(marker);
                this.markers.push(marker);
            }

            this.startMarker = document.createElement("div");
            this.startMarker.classList.add("iqel-timeline-start");

            this.startDate = document.createElement("div");
            this.startDate.classList.add("iqel-start-date");
            this.startTime = document.createElement("div");
            this.startTime.classList.add("iqel-start-time");
            this.startMarker.appendChild(this.startDate);
            this.startMarker.appendChild(this.startTime);

            this.parent.appendChild(this.canvas);
            if (this.mirrored) {
                this.parent.classList.add("mirrored");
            } else {
                this.parent.classList.remove("mirrored");
            }
            this.parent.appendChild(this.startMarker);

            const { getFontStyle } = shmi.requires("visuals.controls.iqtrend.common");
            this.fontStyle = getFontStyle(this.markers[0]);
            const { getTextDimensions } = shmi.requires("visuals.controls.iqtrend.common");
            this.markerWidth = Math.ceil((Math.max(65, getTextDimensions(this.maxMarkerFormat, this.fontStyle).width) + 10) / 20) * 20;

            {
                const parentRect = this.parent.getBoundingClientRect();
                this.width = parentRect.width - 2;
                this.height = this.canvas.offsetHeight - 2;
                const startMarkerRect = this.startMarker.getBoundingClientRect();
                this.startMarkerOffset = (startMarkerRect.left - parentRect.left) + startMarkerRect.width;
            }

            this.min = min;
            this.max = max;
            this.numSteps = numSteps;

            updateSize(this);
            updateStroke(this);
            updateStartMarker(this);

            this.context = this.canvas.getContext("2d");
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = entry.contentRect.width - 2;
                this.height = this.canvas.offsetHeight - 2;
                const parentRect = this.parent.getBoundingClientRect(),
                    startMarkerRect = this.startMarker.getBoundingClientRect();
                this.startMarkerOffset = (startMarkerRect.left - parentRect.left) + startMarkerRect.width;
                updateSize(this);
                updateStroke(this);
                this.draw();
            });

            this.observer.observe(this.parent);
            this.themeToken = shmi.listen("webiq-theme-settings", () => {
                updateStroke(this);
                this.draw();
            });

            this.resizeListener.enable();
            this.draw();
        }
        /**
         * get timeline dimensions (min, max)
         *
         * @return {object} timeline dimensions
         */
        getDimensions() {
            return {
                min: this.min,
                max: this.max
            };
        }
        /**
         * get scale factor
         *
         * @return {number} scale factor
         */
        getScale() {
            return this.width / (this.max - this.min);
        }
        /**
         * set timeline options
         *
         * @param {object} options timeline options
         */
        setOptions(options) {
            if (options.x) {
                if (typeof options.x.min === "number") {
                    this.min = options.x.min;
                }
                if (typeof options.x.max === "number") {
                    this.max = options.x.max;
                }
                this.updateCallback({ min: this.min, max: this.max });
                this.draw();
            }
        }
        /**
         * draw timeline
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            this.context.lineWidth = 1;
            this.context.strokeStyle = this.strokeStyle;
            this.context.beginPath();
            if (this.mirrored) {
                this.context.moveTo(1, this.height + 0.5);
                this.context.lineTo(this.width + 1, this.height + 0.5);
            } else {
                this.context.moveTo(1, 1.5);
                this.context.lineTo(this.width + 1, 1.5);
            }

            const targetSteps = Math.max(1, Math.min(this.numSteps, Math.floor(this.width / this.markerWidth)));
            const stepInfo = getRangeInfo(this.min, this.max, targetSteps),
                numMarkers = stepInfo.range / stepInfo.step + 1,
                scale = this.getScale(),
                stepDistance = stepInfo.step * scale;

            for (let i = 0; i < numMarkers && i < this.markers.length; i++) {
                const markerValue = stepInfo.start + (i * stepInfo.step);
                if (markerValue >= this.min && markerValue <= this.max) {
                    const x = drawScaleMarker(this, scale, markerValue, this.height);

                    drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);

                    if (x > this.startMarkerOffset + (this.markerWidth / 2)) {
                        this.markers[i].textContent = getMarkerText(this, stepInfo, markerValue);
                        this.markers[i].style.left = `${x - (this.markerWidth / 2)}px`;
                        this.markers[i].style.width = `${this.markerWidth}px`;
                        this.markers[i].style.display = "block";
                    } else {
                        this.markers[i].style.display = "none";
                    }
                } else {
                    if (i === 0) {
                        drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);
                    }
                    this.markers[i].style.display = "none";
                }
            }
            this.markers.slice(numMarkers, this.markers.length).forEach((marker) => {
                marker.style.display = "none";
            });

            this.context.stroke();
            updateStartMarker(this);
        }
        /**
         * remove timeline
         *
         */
        remove() {
            if (this.parent) {
                this.observer.disconnect();
                this.parent.removeChild(this.canvas);
                this.markers.forEach((marker) => {
                    this.parent.removeChild(marker);
                });
                this.parent.removeChild(this.startMarker);
                this.parent = null;
            }

            if (this.themeToken) {
                this.themeToken.unlisten();
                this.themeToken = null;
            }
            this.resizeListener.disable();
        }
    };
}());

/**
 * Module to implement trend data operation to queue in TaskQueue.
 *
 * @module visuals/controls/iqtrend/trendtask
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.trendtask",
        /** @lends module:visuals/controls/iqtrend/trendtask */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const states = shmi.requires("visuals.controls.iqtrend.common").taskStates;

    module.TrendTask = class TrendTask {
        /**
         * Creates an instance of TrendTask.
         * @param {string} type type of task
         * @param {object} parameters task execution parameters
         * @param {function} asyncTaskFunction function that performs task
         */
        constructor(type, parameters, asyncTaskFunction) {
            this._type = type;
            this._parameters = parameters;
            this._state = states.CREATED;
            this._taskFunction = asyncTaskFunction;
            this._result = null;
            this._error = null;
        }
        /**
         * get current state of task
         *
         */
        get state() {
            return this._state;
        }
        /**
         * set state of task
         *
         */
        set state(state) {
            this._state = state;
        }
        /**
         * get task type
         *
         * @readonly
         */
        get type() {
            return this._type;
        }
        /**
         * get task parameters
         *
         * @readonly
         */
        get parameters() {
            return this._parameters;
        }
        /**
         * get task result
         *
         * @readonly
         */
        get result() {
            return this._result;
        }
        /**
         * get task error information
         *
         * @readonly
         */
        get error() {
            return this._error;
        }
        /**
         * run task
         *
         * @return {Promise} promise resolving when task has been completed
         */
        async run() {
            if (this._state !== states.CREATED) {
                throw new Error("Tried to run task that is not in initial 'CREATED' state.");
            }
            this._state = states.RUNNING;
            try {
                this._result = await this._taskFunction(this._parameters, this);
                this._state = states.DONE;
            } catch (err) {
                this._error = err;
                console.error("Error executing task:", err, this.type, this.parameters);
                this._state = states.ERROR;
            }

            return this._result;
        }
    };
}());

/**
 * Module to implement numeric x-axis for iq-trend.
 *
 * @module visuals/controls/iqtrend/xaxis
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.xaxis",
        /** @lends module:visuals/controls/iqtrend/xaxis */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    /**
     * initial distance between labeled steps
     * @type {number}
     */
    let stepSize = 75;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
        const style = getComputedStyle(self.parent);
        self.strokeStyle = style.getPropertyValue("--timeline-color");
    }

    /**
     * available steps between labeled values
     * @type {number[]}
     */
    const stepWidths = [
        1,
        0.5,
        0.25,
        0.2
    ];

    /**
     * convert value to specified number of decimal digits
     *
     * @param {number} value input value
     * @param {number} digits maximum number of decimal digits
     * @return {number} converted value
     */
    function toDigits(value, digits) {
        return parseFloat(value.toFixed(digits));
    }

    /**
     * get step size for specified range
     *
     * @param {number} min minimum x-axis value
     * @param {number} max maximum x-axis value
     * @param {number} maxNumSteps maximum number of steps
     * @return {number} step size
     */
    function getSimpleStep(min, max, maxNumSteps) {
        const range = max - min;
        let order = Math.ceil(Math.log10(range));

        let step = 1;
        let numSteps = 0;
        while (numSteps < maxNumSteps) {
            for (let i = 0; i < stepWidths.length; i++) {
                const testStep = stepWidths[i] * (10 ** order);
                numSteps = range / testStep;
                if (numSteps <= maxNumSteps) {
                    step = testStep;
                } else {
                    break;
                }
            }
            order -= 1;
        }

        return step;
    }
    /**
     * get range info for specified value range
     *
     * @param {number} min minimum x-axis value
     * @param {number} max maximum x-axis value
     * @param {number} maxNumSteps maximum number of steps
     * @return {object} range info
     */
    function getRangeInfo(minY, maxY, maxNumSteps) {
        const step = getSimpleStep(minY, maxY, maxNumSteps),
            digits = getDigits(step),
            startY = Math.floor(minY / step) * step,
            endY = startY + Math.ceil((maxY - startY) / step) * step;

        return {
            step: toDigits(step, digits),
            start: toDigits(startY, digits),
            range: toDigits(endY - startY, digits)
        };
    }

    /**
     * get number of decimal digits
     *
     * @param {number} value input value
     * @return {number} number of decimal digits
     */
    function getDigits(value) {
        const valueString = String(value);
        let valueDigits = valueString.indexOf(".");
        if (valueDigits !== -1) {
            valueDigits = valueString.length - valueDigits - 1;
        } else {
            valueDigits = 0;
        }
        return valueDigits;
    }

    /**
     * get x-coordinate for specified value
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value input value
     * @return {number} x-coordinate
     */
    function getCoordinate(self, scale, value) {
        return (value - self.min) * scale + 1;
    }

    /**
     * draw scale marker
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value input value
     * @param {number} width marker width
     * @return {number} x-coordinate
     */
    function drawScaleMarker(self, scale, value, width) {
        const x = getCoordinate(self, scale, value);

        if (self.mirrored) {
            self.context.moveTo(x, self.height + 1);
            self.context.lineTo(x, (width) + 1);
        } else {
            self.context.moveTo(x, 1 + width);
            self.context.lineTo(x, 1);
        }

        return x;
    }

    /**
     * draw small markers between labeled markers
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {object} stepInfo stepping info
     * @param {number} stepDistance step distance
     * @param {number} markerValue value at marker
     * @param {number} index marker index
     */
    function drawSmallMarkers(self, scale, stepInfo, stepDistance, markerValue, index) {
        if (stepDistance > 100) {
            const smallDiff = stepInfo.step / 4;
            if (index === 0) {
                for (let j=0; j<3; j++) {
                    const smallMarkerValue = markerValue - (j + 1) * smallDiff;
                    if (smallMarkerValue >= self.min) {
                        drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                    }
                }
            }
            for (let j=0; j<3; j++) {
                const smallMarkerValue = markerValue + (j + 1) * smallDiff;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        } else if (stepDistance > 75) {
            if (index === 0) {
                const smallMarkerValue = markerValue - stepInfo.step / 2;
                if (smallMarkerValue >= self.min) {
                    drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                }
            }

            {
                const smallMarkerValue = markerValue + stepInfo.step / 2;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        }
    }

    module.XAxis = class XAxis {
        /**
         * Creates an instance of XAxis.
         * @param {HTMLElement} element parent element to attach x-axis to
         * @param {object} {
         *             min = 0,
         *             max = 100,
         *             numSteps = 21,
         *             mirrored = false,
         *             strokeStyle = "rgb(0, 0, 0)"
         *         } x-axis options
         */
        constructor(element, {
            min = 0,
            max = 100,
            numSteps = 21,
            mirrored = false
        }, updateCallback) {
            this.canvas = document.createElement("canvas");
            this.wrapper = document.createElement("div");
            this.wrapper.classList.add("iqel-xaxis-wrapper");
            this.parent = element;
            this.canvas.classList.add("iqel-xaxis-canvas");
            this.updateCallback = typeof updateCallback === "function" ? updateCallback : () => {};

            this.mirrored = mirrored;
            if (this.mirrored) {
                this.wrapper.classList.add("mirrored");
            }

            this.markers = [];
            for (let i = 0; i < numSteps + 1; i++) {
                const marker = document.createElement("div");
                marker.classList.add("iqel-xaxis-label");
                marker.style.display = "none";
                this.wrapper.appendChild(marker);
                this.markers.push(marker);
            }

            this.wrapper.appendChild(this.canvas);
            this.parent.appendChild(this.wrapper);

            const { getFontStyle } = shmi.requires("visuals.controls.iqtrend.common");
            this.fontStyle = getFontStyle(this.markers[0]);

            const rect = this.parent.getBoundingClientRect();
            this.width = rect.width - 2;
            this.height = this.canvas.offsetHeight - 2;

            this.min = min;
            this.max = max;
            this.numSteps = numSteps;

            updateSize(this);
            this.context = this.canvas.getContext("2d");
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = entry.contentRect.width - 2;
                this.height = this.canvas.offsetHeight - 2;
                updateSize(this);
                this.draw();
            });

            this.observer.observe(this.parent);
            this.draw();
        }
        /**
         * set x-axis options
         *
         * @param {object} options x-axis options
         */
        setOptions(options) {
            if (options.x) {
                if (typeof options.x.min === "number") {
                    this.min = options.x.min;
                }
                if (typeof options.x.max === "number") {
                    this.max = options.x.max;
                }
                this.updateCallback({ min: this.min, max: this.max });
                this.draw();
            }
        }
        /**
         * draw x-axis
         *
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            this.context.lineWidth = 1;
            this.context.strokeStyle = this.strokeStyle;
            this.context.beginPath();
            if (this.mirrored) {
                this.context.moveTo(1, this.height + 0.5);
                this.context.lineTo(this.width + 1, this.height + 0.5);
            } else {
                this.context.moveTo(1, 1.5);
                this.context.lineTo(this.width + 1, 1.5);
            }
            const { getTextDimensions } = shmi.requires("visuals.controls.iqtrend.common");
            stepSize = Math.max(65, getTextDimensions(this.min, this.fontStyle).width, getTextDimensions(this.max, this.fontStyle).width) + 10;
            const targetSteps = Math.max(1, Math.min(this.numSteps, Math.floor(this.width / stepSize)));

            const stepInfo = getRangeInfo(this.min, this.max, targetSteps),
                stepDigits = getDigits(stepInfo.step),
                numMarkers = toDigits(stepInfo.range / stepInfo.step, stepDigits) + 1,
                scale = this.width / (this.max - this.min),
                stepDistance = stepInfo.step * scale;

            for (let i = 0; i < numMarkers && i < this.markers.length; i++) {
                const markerValue = toDigits(stepInfo.start + (i * stepInfo.step), stepDigits);
                if (markerValue >= this.min && markerValue <= this.max) {
                    const x = drawScaleMarker(this, scale, markerValue, this.height);
                    drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);

                    this.markers[i].textContent = toDigits(markerValue, stepDigits);
                    this.markers[i].style.left = `${x - (stepSize / 2)}px`;
                    this.markers[i].style.width = `${stepSize}px`;
                    this.markers[i].style.display = "block";
                } else {
                    if (i === 0) {
                        drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);
                    }
                    this.markers[i].style.display = "none";
                }
            }
            this.markers.slice(numMarkers, this.markers.length).forEach((marker) => {
                marker.style.display = "none";
            });
            this.context.stroke();
        }
        /**
         * get x-axis dimensions
         *
         * @return {object} dimensions (min, max)
         */
        getDimensions() {
            return {
                min: this.min,
                max: this.max
            };
        }
        /**
         * get scale factor
         *
         * @return {number} scale factor
         */
        getScale() {
            return this.width / (this.max - this.min);
        }
        /**
         * remove x-axis
         *
         */
        remove() {
            if (this.parent) {
                this.observer.disconnect();
                this.parent.removeChild(this.wrapper);
                this.parent = null;
            }
        }
    };
}());

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *      'class-name': 'user-info',
 *      'name': null,
 *      'template': 'custom/controls/user-info',
 *      'label': uiType,
 *      'show-name': false,
 *      'show-group': false
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = 'iqUserInfo', // control name in camel-case
        uiType = 'iq-user-info', // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        'class-name': 'iq-user-info iq-variant-01',
        'name': null,
        'template': 'default/iq-user-info.iq-variant-01',
        'label': "[Label]",
        'show-name': false,
        'show-group': false,
        'action': null
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires('visuals.tools.logging').createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    /**
     * Displays current user
     *
     * @param  {object} self
     */
    function displayUser(self) {
        const domElName = shmi.getUiElement('user-info-name', self.element);

        // Abort if HTML element is missing
        if (!domElName) {
            fLog('DOM-Element "user-info-name" not found!');
            return;
        }

        // Check show-name property and set to false if undefined
        if (!self.config['show-name']) {
            self.config['show-name'] = false;
        }
        // Show username or name and surname if available
        if (self.config['show-name'] === false) {
            domElName.textContent = self.vars.user.username;
        } else if (self.config['show-name'] === true) {
            if (self.vars.user.name && self.vars.user.surname) {
                domElName.textContent = self.vars.user.name + ' ' + self.vars.user.surname;
            } else if (!self.vars.user.name) {
                domElName.textContent = self.vars.user.surname;
            } else if (!self.vars.user.surname) {
                domElName.textContent = self.vars.user.name;
            } else {
                domElName.textContent = 'First Name and/or Last Name not set!';
            }
        }
    }

    /**
     * Deletes Designer-usergroups and displays extant usergroup
     *
     * @param  {object} self
     */
    function setUserGroup(self) {
        let domElGroup = shmi.getUiElement('user-info-group', self.element),
            primUserGrp = null,
            { userGroups } = self.vars.user;

        // Abort if HTML element is missing
        if (!domElGroup) {
            fLog('Dom-Element "user-info-group" not found!');
            return;
        }

        // check for internal groups and delete from list
        userGroups = userGroups.filter((group) => group.indexOf('@') !== 0);

        // add default in case of no matching group
        if (userGroups.length > 0) {
            primUserGrp = userGroups[0];
        } else {
            primUserGrp = 'Default';
        }
        domElGroup.textContent = primUserGrp;
    }
    /**
     * Gets userdata (name and surname) from connect and calls setUser()
     * @param  {object} self
     */
    function getUser(self) {
        const { username: curUserName } = self.vars.user;

        self.imports.request('user.list', {}, function(response, err) {
            if (response && Array.isArray(response.users)) {
                const userdata = response.users.find(({ username }) => username === curUserName);

                if (!userdata) {
                    fLog('User not found in remote user list');
                    return;
                }

                self.vars.user.name = userdata.first_name;
                self.vars.user.surname = userdata.last_name;

                displayUser(self);
            } else {
                fLog('[UserManager] could not retrieve userlist:', err.category, err.errc, err.message);
                fLog('User name and surname cannot be displayed!');
            }
        });
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        configSchema: null,
        /* instance variables */
        vars: {
            user: {
                userGroups: null,
                name: null,
                surname: null,
                username: null
            },
            action: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            request: 'visuals.tools.connect.request'
        },

        /* array of custom event types fired by this control */
        events: ['click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            onInit: function() {
                const self = this;
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }

                if (Array.isArray(self.config.action) && self.config.action.length > 0) {
                    shmi.addClass(self.element, 'clickable');
                } else {
                    shmi.removeClass(self.element, 'clickable');
                }

                const eventFunctions = {
                    onClick: function(x, y, e) {
                        self.fire('click', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }
                    }
                };

                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.element, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.element, eventFunctions));
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;
                self.vars.user.userGroups = shmi.visuals.session.UserManager.currentUser.groupList;
                self.vars.user.username = shmi.visuals.session.UserManager.currentUser.name;

                getUser(self);
                // displayUser() called from getUser
                if (self.config['show-group'] === true) {
                    setUserGroup(self);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                shmi.addClass(self.element, 'locked');
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.removeClass(self.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires('visuals.tools.control-generator');
    cg.generate(definition);
})();

/**
 * Implementation of the "Add or Edit User" dialog used by the
 * `user-controller` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME),
        /** Name of the ui controller */
        CONTROLLER_NAME = "iq-add-user-dialog";

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Closes the dialog and resolves the dialog promise exactly once.
     *
     * @param {*} controllerState State of the controller.
     * @param {*} result Result to resolve the promise with.
     */
    function closeDialog(controllerState, result) {
        if (controllerState.active && !controllerState.data.isDone) {
            controllerState.data.isDone = true;
            controllerState.getInstance("dlgBox").hide();
            controllerState.data.resolve(result);
        }
    }

    /**************************
     * EVENT HANDLERS | CLICK *
     **************************/
    const buttonHandlers = {
        /**
         * Click handler for the "Create User" button.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onCreateUser: function onCreateUser(controllerState) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { setLockedSlots, setUnlockedSlots } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            } else if (typeof controllerState.data.formData.inputUsername !== "string" || controllerState.data.formData.inputUsername.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputUsername.startsWith(" ") || controllerState.data.formData.inputUsername.endsWith(" ") || controllerState.data.formData.inputUsername.includes("  ")) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameInvalid}");
            } else if (typeof controllerState.data.formData.inputPassword !== "string" || controllerState.data.formData.inputPassword.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.passwordMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputPassword !== controllerState.data.formData.inputPasswordRep) {
                throw new shmi.LocalizedError("${V_PASSWORD_REPEAT_NOT_EQUAL}");
            } else if (controllerState.data.formData.inputPassword.length < shmi.c("MIN_PASSWORD_LENGTH")) {
                throw new shmi.LocalizedError("${V_PASSWORD_TOO_SHORT}");
            }

            const username = controllerState.data.formData.inputUsername,
                password = controllerState.data.formData.inputPassword,
                additionalProperties = {
                    firstName: controllerState.data.formData.inputFirstName,
                    lastName: controllerState.data.formData.inputLastName,
                    userGroups: controllerState.data.formData.inputUserGroup ? [controllerState.data.formData.inputUserGroup] : [],
                    localeId: controllerState.data.formData.inputLocale,
                    passwordValidityDuration: controllerState.data.formData.inputPasswordValidityDuration,
                    autoLogoutDelay: controllerState.data.formData.inputAutoLogoutDelay,
                    description: controllerState.data.formData.inputDescription
                };

            setUnlockedSlots(controllerState, []);

            return UserApiBinding.createUser(username, password, additionalProperties).
                then(closeDialog.bind(null, controllerState, true)).
                catch((reason) => {
                    setLockedSlots(controllerState, []);
                    throw reason;
                });
        },
        /**
         * Click handler for the "apply" button. Applies changes to an existing user.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onApplyUser: function onApplyUser(controllerState) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { setLockedSlots, setUnlockedSlots } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            } else if (typeof controllerState.data.formData.inputUsername !== "string" || controllerState.data.formData.inputUsername.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputUsername.startsWith(" ") || controllerState.data.formData.inputUsername.endsWith(" ") || controllerState.data.formData.inputUsername.includes("  ")) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameInvalid}");
            } else if (controllerState.data.formData.inputPassword !== controllerState.data.formData.inputPasswordRep) {
                throw new shmi.LocalizedError("${iq-user-controller.error.passwordMismatch}");
            }

            if (typeof controllerState.data.formData.inputPassword !== "undefined") {
                if (typeof controllerState.data.formData.inputPassword !== "string" || controllerState.data.formData.inputPassword.length === 0) {
                    throw new shmi.LocalizedError("${iq-user-controller.error.passwordMustNotBeEmpty}");
                } else if (controllerState.data.formData.inputPassword.length < shmi.c("MIN_PASSWORD_LENGTH")) {
                    throw new shmi.LocalizedError("${V_PASSWORD_TOO_SHORT}");
                }
            }

            const username = controllerState.data.formData.inputUsername,
                additionalProperties = {
                    firstName: controllerState.data.formData.inputFirstName,
                    lastName: controllerState.data.formData.inputLastName,
                    userGroups: controllerState.data.formData.inputUserGroup ? [controllerState.data.formData.inputUserGroup] : [],
                    localeId: controllerState.data.formData.inputLocale,
                    password: controllerState.data.formData.inputPassword,
                    passwordValidityDuration: controllerState.data.formData.inputPasswordValidityDuration,
                    autoLogoutDelay: controllerState.data.formData.inputAutoLogoutDelay,
                    description: controllerState.data.formData.inputDescription
                };

            setUnlockedSlots(controllerState, []);

            return UserApiBinding.setUser(username, additionalProperties).
                then(closeDialog.bind(null, controllerState, true)).
                catch((reason) => {
                    setLockedSlots(controllerState, ["inputUsername"]);
                    throw reason;
                });
        },
        /**
         * Click handler for the "close" button. Resolves the main promise.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onClose: function onClose(controllerState) {
            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            }

            closeDialog(controllerState);
        }
    };

    /**
     * Creates the dialog.
     *
     * @param {string} [userData] If set, the forms will be populated with the
     *  given user data. If `editMode` is not `true`, the `Username` form will
     *  have a ` (2)` appended. This allows for easy cloning of existing users.
     * @param {boolean} [editMode] If set to `true`, the dialog will edit
     *  existing users instead of creating new ones. The username input field
     *  is locked in this mode.
     * @param {boolean} [hideAdmin] If set to `true`, the dialog will not allow to
     *  set any usergroup being a member of the 'admin' group
     * @param {function} resolve Forwarded `resolve` callback from a promise.
     * @param {function} reject Forwarded `reject` callback from a promise.
     */
    function createDialog(userData, editMode, hideAdmin, resolve, reject) {
        const appRoot = shmi.getControlByElement(document.body.firstElementChild),
            { makeControllerDescriptor } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

        appRoot.addControl([
            {
                "ui": "dialog-box",
                "config": {
                    "name": "iq-add-user-dialog",
                    "title": editMode ? "${iq-user-controller.title.editUserDialog}" : "${iq-user-controller.title.createUserDialog}",
                    "template": "default/dialog-box",
                    "content-template": null,
                    "class-name": `dialog-box add-or-edit-user ${editMode ? "hide-passwords" : ""}`,
                    "top-level": false,
                    "_controllers_": [
                        {
                            "name": CONTROLLER_NAME,
                            "slot": "dlgBox"
                        }
                    ]
                },
                "children": [
                    {
                        "ui": "container",
                        "config": {
                            "flex-orientation": "column",
                            "class-name": "iq-container full-height"
                        },
                        "children": [
                            {
                                "ui": "container",
                                "config": {
                                    "flex-orientation": "column",
                                    "class-name": "iq-container dialog-form-container",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${user_name}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputUsername"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${iq-user-controller.label.firstName}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputFirstName"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${iq-user-controller.label.lastName}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputLastName"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-select-box",
                                        "config": {
                                            "class-name": "iq-select-box iq-variant-02",
                                            "template": "default/iq-select-box.iq-variant-02",
                                            "label": "${iq-user-controller.label.userGroup}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputUserGroup"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-select-box",
                                        "config": {
                                            "class-name": "iq-select-box iq-variant-02",
                                            "template": "default/iq-select-box.iq-variant-02",
                                            "label": "${iq-user-controller.label.localeId}",
                                            "options": Object.keys(shmi.visuals.session.localeInfo.locales).map((localeId) => ({ value: localeId, label: localeId })),
                                            "value": shmi.visuals.session.localeInfo.default,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputLocale"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "container",
                                        "config": {
                                            "type": "flex",
                                            "class-name": "iq-container no-shrink",
                                            "auto-margin": true
                                        },
                                        "children": [
                                            {
                                                "ui": "iq-input-field",
                                                "config": {
                                                    "class-name": "iq-input-field iq-variant-02 flex-1",
                                                    "template": "default/iq-input-field.iq-variant-02",
                                                    "label": "${iq-user-controller.label.passwordExpiry}",
                                                    "unit-text": null,
                                                    "_controllers_": [
                                                        {
                                                            "name": CONTROLLER_NAME,
                                                            "slot": "inputPasswordValidityDuration"
                                                        }
                                                    ]
                                                }
                                            },
                                            {
                                                "ui": "iq-input-field",
                                                "config": {
                                                    "class-name": "iq-input-field iq-variant-02 flex-1",
                                                    "template": "default/iq-input-field.iq-variant-02",
                                                    "label": "${iq-user-controller.label.autoLogoutDelay}",
                                                    "unit-text": null,
                                                    "_controllers_": [
                                                        {
                                                            "name": CONTROLLER_NAME,
                                                            "slot": "inputAutoLogoutDelay"
                                                        }
                                                    ]
                                                }
                                            }
                                        ]
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05 password-input",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPassword"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05 password-input",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${confirm_new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPasswordRep"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02 textarea",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${iq-user-controller.label.description}",
                                            "unit-text": null,
                                            "multiline": true,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputDescription"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            },
                            {
                                "ui": "container",
                                "config": {
                                    "class-name": "iq-container dialog-buttons-container",
                                    "flex-primary-align": "end",
                                    "flex-orientation": "row",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01",
                                            "label": "${lbl_cancel}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnCancel"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01 filled",
                                            "label": editMode ? "${iq-user-controller.label.apply}" : "${iq-user-controller.label.add}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnApply"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ],
                "controller": makeControllerDescriptor({
                    name: CONTROLLER_NAME,
                    slots: {
                        inputUsername: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputFirstName: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputLastName: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputUserGroup: {
                            ui: "iq-select-box",
                            formType: "string"
                        },
                        inputLocale: {
                            ui: "iq-select-box",
                            formType: "string"
                        },
                        inputPasswordValidityDuration: {
                            ui: "iq-input-field",
                            formType: "number"
                        },
                        inputAutoLogoutDelay: {
                            ui: "iq-input-field",
                            formType: "number"
                        },
                        inputPassword: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputPasswordRep: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputDescription: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        btnApply: {
                            ui: "iq-button",
                            events: {
                                click: editMode ? buttonHandlers.onApplyUser : buttonHandlers.onCreateUser
                            }
                        },
                        btnCancel: {
                            ui: "iq-button",
                            events: {
                                click: buttonHandlers.onClose
                            }
                        },
                        dlgBox: {
                            ui: "dialog-box"
                        }
                    },
                    data: {
                        /** Promise reject callback. */
                        reject: reject,
                        /** Promise resolve callback. */
                        resolve: resolve,
                        /**
                         * Flag determining whether or not the dialog promise
                         * has already been resolved/rejected.
                         */
                        isDone: false,
                        formData: {}
                    },
                    onEnable: function onEnable(state) {
                        const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                            { setLockedSlots, setUnlockedSlots, reloadFormValues } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

                        setUnlockedSlots(state, ["btnCancel"]);
                        const listPromise = UserApiBinding.listGroups().then((response) => {
                            const groupMap = {};

                            response.usergroups.forEach((group) => {
                                groupMap[group.groupname] = [group.groupname];
                                group._groupdata.member_groups.forEach((mGroup) => {
                                    groupMap[group.groupname].push(mGroup);
                                });
                            });
                            state.getInstance("inputUserGroup").setOptions(
                                [{ value: null, label: "---" }].concat(
                                    response.usergroups.filter((userGroup) => {
                                        if (hideAdmin) {
                                            return !groupMap[userGroup.groupname].includes("admin");
                                        }
                                        return true;
                                    }).map((userGroup) => ({
                                        value: userGroup.groupname,
                                        label: userGroup.groupname
                                    }))
                                )
                            );
                        });

                        Promise.all([listPromise]).then(() => {
                            if (userData) {
                                state.data.formData = {
                                    inputUsername: editMode ? userData.username : null,
                                    inputFirstName: userData.firstName,
                                    inputLastName: userData.lastName,
                                    inputUserGroup: userData.userGroups[0] || null,
                                    inputLocale: userData.localeId,
                                    inputPasswordValidityDuration: userData.passwordValidityDuration,
                                    inputAutoLogoutDelay: userData.autoLogoutDelay,
                                    inputDescription: userData.description
                                };

                                reloadFormValues(state, [], []);
                            }

                            setLockedSlots(state, editMode ? ["inputUsername"] : []);
                        });
                    },
                    onDisable: function onDisable(state) {
                        if (!state.data.isDone) {
                            state.data.resolve();
                        }

                        shmi.deleteControl(state.getInstance("dlgBox"));
                    },
                    onError: function onError(state, reason) {
                        if (reason.category === "shmi:connect:api:generic" && reason.errc === 14) {
                            throw new shmi.LocalizedError("${iq-user-controller.error.invalidParameter}");
                        } else if (reason.category === "shmi:connect:api:user" && reason.errc === 4) {
                            throw new shmi.LocalizedError("${iq-user-controller.error.invalidPasswordExpiryDuration}");
                        }

                        throw reason;
                    }
                })
            }
        ], (err, controls) => {
            if (err) {
                reject(err);
            } else {
                controls[0].show();
            }
        });
    }

    /**
     * Creates an administrative dialog to add or edit users.
     *
     * @param {string} [userData] If set, the forms will be populated with the
     *  given user data. If `editMode` is not `true`, the `Username` form will
     *  have a ` (2)` appended. This allows for easy cloning of existing users.
     * @param {boolean} [editMode] If set to `true`, the dialog will edit
     *  existing users instead of creating new ones. The username input field
     *  is locked in this mode.
     * @param {boolean} [hideAdmin] If set to `true`, the dialog will not allow to
     *  set any usergroup being a member of the 'admin' group
     * @return {Promise<?boolean>}
     */
    module.openUserAddEditDialog = function openUserAddEditDialog(user, editMode, hideAdmin) {
        return new Promise(createDialog.bind(null, user, editMode, hideAdmin));
    };
}());

/**
 * @module visuals/controls/iq-user-management/controller-tools
 */
(function() {
    const MODULE_NAME ="visuals.controls.iq-user-management.controller-tools",
        /** @lends module:visuals/controls/iq-user-management/controller-tools */
        module = shmi.pkg(MODULE_NAME);

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Change handlers for form data.
     */
    const formChangeHandlers = {
        /**
         * Handles change events for form strings. Empty string are treated as
         * `null` values.
         *
         * @param {*} controllerState State of the controller.
         * @param {string} slotName Name of the slot that fired the event. The
         *  new form value is assigned to this key.
         * @param {*} event The event.
         */
        string: function string(controllerState, slotName, event) {
            if (!controllerState.active || !controllerState.data.formData) {
                return;
            }

            const oldValueIsEmpty = (controllerState.data.formData[slotName] === null),
                // Check if the instance is locked. If so, ignore the change.
                ignoreChange = (() => {
                    const instance = controllerState.getInstance(slotName);
                    return instance && instance.locked;
                })(),
                // The new value.
                newValue = event.detail.value === "" ? null : event.detail.value;

            if (oldValueIsEmpty && (newValue === null || newValue.length === 0)) {
                // We assume the empty string and an empty value are the same
                // so don't set the changed flag.
            } else if (controllerState.data.formData[slotName] !== newValue) {
                controllerState.data.formData[slotName] = newValue;
                if (!ignoreChange) {
                    controllerState.data.hasChanged = true;
                }
            }
        },
        /**
         * Handles change events for form numbers. Strings are parsed as
         * floats and empty strings are treated as `null` values.
         *
         * @param {*} controllerState State of the controller.
         * @param {string} slotName Name of the slot that fired the event. The
         *  new form value is assigned to this key.
         * @param {*} event The event.
         */
        number: function number(controllerState, slotName, event) {
            if (!controllerState.active || !controllerState.data.formData) {
                return;
            }

            const oldValueIsEmpty = (controllerState.data.formData[slotName] === null),
                // Check if the instance is locked. If so, ignore the change.
                ignoreChange = (() => {
                    const instance = controllerState.getInstance(slotName);
                    return instance && instance.locked;
                })(),
                // The new value.
                newValue = (() => {
                    if (typeof event.detail.value === "string") {
                        if (event.detail.value === "") {
                            return null;
                        }

                        return parseFloat(event.detail.value);
                    }

                    return event.detail.value;
                })();

            if (oldValueIsEmpty && newValue === 0) {
                // We assume 0 and an empty value are the same so don't set the
                // changed flag.
                controllerState.data.formData[slotName] = newValue;
            } else if (controllerState.data.formData[slotName] !== newValue) {
                controllerState.data.formData[slotName] = newValue;
                if (!ignoreChange) {
                    controllerState.data.hasChanged = true;
                }
            }
        },
        /**
         * Handles change events for form booleans. Any value is converted to
         * boolean representation.
         *
         * @param {*} controllerState State of the controller.
         * @param {string} slotName Name of the slot that fired the event. The
         *  new form value is assigned to this key.
         * @param {*} event The event.
         */
        boolean: function boolean(controllerState, slotName, event) {
            if (!controllerState.active || !controllerState.data.formData) {
                return;
            }

            const oldValueIsEmpty = (controllerState.data.formData[slotName] === null),
                // Check if the instance is locked. If so, ignore the change.
                ignoreChange = (() => {
                    const instance = controllerState.getInstance(slotName);
                    return instance && instance.locked;
                })(),
                newValue = !!event.detail.value;

            if (oldValueIsEmpty && !newValue) {
                // We assume false and an empty value are the same so don't set
                // the changed flag.
                controllerState.data.formData[slotName] = newValue;
            } else if (controllerState.data.formData[slotName] !== newValue) {
                controllerState.data.formData[slotName] = newValue;
                if (!ignoreChange) {
                    controllerState.data.hasChanged = true;
                }
            }
        }
    };

    /**
     * Widgets supported by the form addon.
     */
    module.uiFormWidgets = {
        "input-field": true,
        "iq-input-field": true,
        "checkbox": (ctrl, value) => ctrl.setValue(value ? 1 : 0),
        "iq-checkbox": (ctrl, value) => ctrl.setValue(value ? 1 : 0),
        "select-box": true,
        "iq-select-box": true,
        "flip-switch": true,
        "iq-flip-switch": true,
        "default": (ctrl, value) => ctrl.setValue(value)
    };

    /**
     * Sets a widget's value.
     *
     * @param {BaseControl} ctrl Widget whose value to set.
     * @param {*} value Value to set.
     */
    function setFormWidgetValue(ctrl, value) {
        const setter = module.uiFormWidgets[ctrl.uiType],
            defaultSetter = module.uiFormWidgets.default;

        if (typeof setter === "undefined") {
            defaultSetter(ctrl, value);
        } else if (typeof setter === "boolean") {
            if (setter) {
                defaultSetter(ctrl, value);
            }
        } else {
            setter(ctrl, value);
        }
    }

    /**
     * Resets form data of a ui controller.
     *
     * @param {*} controllerState
     * @param {?object} [formData] Form data to use as replacement.
     * @param {?string[]} [uiTypes] Array of ui types used in the form.
     *  Defaults to ["input-field", "checkbox"].
     */
    module.resetFormValues = function resetFormValues(controllerState, formData, uiTypes) {
        const formUiTypes = uiTypes || Object.keys(module.uiFormWidgets);

        controllerState.data.formData = formData || {};

        Object.keys(controllerState.slots).forEach((slotName) => {
            const ctrl = controllerState.getInstance(slotName);
            if (ctrl) {
                if (formUiTypes.includes(ctrl.uiType)) {
                    ctrl.setValue(null);
                    controllerState.data.formData[slotName] = null;
                } else if (controllerState.data.formData[slotName]) {
                    ctrl.setValue(controllerState.data.formData[slotName]);
                }
            }
        });
    };

    /**
     * Reloads form data from the controller state into the corresponding
     * slots.
     *
     * @param {object} state
     * @param {string[]} except Slots to not reload data for.
     * @param {string[]} formExtras Slots to reload even though they are not
     *  an `input-field` or a `checkbox`.
     */
    module.reloadFormValues = function reloadFormValues(state, except, formExtras) {
        const formUiTypes = Object.keys(module.uiFormWidgets).concat(formExtras || []);

        Object.keys(state.slots).forEach((slotName) => {
            const ctrl = state.getInstance(slotName);
            // Does slot have a widget which has a supported ui type and is not
            // excluded?
            if (ctrl && formUiTypes.indexOf(ctrl.uiType) !== -1 && (except || []).indexOf(slotName) === -1) {
                if (typeof state.data.formData[slotName] === "undefined") {
                    setFormWidgetValue(ctrl, null);
                } else {
                    setFormWidgetValue(ctrl, state.data.formData[slotName]);
                }
            }
        });
    };

    module.addons = module.addons || [];

    // Register addon.
    module.addons.push({
        configName: "formType",
        addonFunc: (formType) => ({
            change: formChangeHandlers[formType]
        })
    });
}());

/**
 * @module visuals/controls/iq-user-management/controller-tools
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-management.controller-tools",
        /** @lends module:visuals/controls/iq-user-management/controller-tools */
        module = shmi.pkg(MODULE_NAME);

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Returns a handler callback for the given slot and type.
     *
     * @param {object} eventHandlers
     * @param {string} type Event type
     * @param {string} slot Name of the slot to return the callback for.
     * @returns {?function} Callback or `null` if no callback for the given
     *  event/slot combination could be found.
     */
    function getSlotCallback(eventHandlers, type, slot) {
        if (typeof eventHandlers !== "object") {
            return null;
        } else if (typeof eventHandlers[type] !== "object") {
            return null;
        } else if (typeof eventHandlers[type][slot] !== "function") {
            return null;
        }

        return eventHandlers[type][slot];
    }

    /**
     * Translates the alternate controller slot configuration to one
     * understood by the controller module. For every event handler that is
     * defined in-place, a new event handler is created and returned. The
     * original slot configuration object is modified.
     *
     * @param {object} slots Slot configuration.
     * @returns {object} Object of in-place defined event handlers.
     */
    function collectAndTranslateEventHandlers(slots) {
        const iterObj = shmi.requires("shmi.visuals.tools.iterate.iterateObject"),
            eventHandlers = {};

        iterObj(slots, (slotConfig, slotName) => {
            // If the slot configuration is an array, it is vanilla and we
            // don't touch it.
            if (Array.isArray(slotConfig.events)) {
                return;
            }

            // Collect the event handlers.
            iterObj(slotConfig.events || {}, (eventHandler, eventType) => {
                if (!shmi.objectHasOwnProperty(eventHandlers, eventType)) {
                    eventHandlers[eventType] = {};
                }

                eventHandlers[eventType][slotName] = eventHandler;
            });
            // Generate an array of event types we're interested in. This
            // converts the slot configuration to it's vanilla form.
            slotConfig.events = Object.keys(slotConfig.events || {});

            // Process addons
            (module.addons || []).forEach(({ configName, addonFunc }) => {
                if (shmi.objectHasOwnProperty(slotConfig, configName)) {
                    const addonEvents = addonFunc(slotConfig[configName], slotName, slotConfig);

                    // Register addon event handlers.
                    iterObj(addonEvents || {}, (eventHandler, eventType) => {
                        if (!eventHandlers[eventType]) {
                            eventHandlers[eventType] = {};
                        }

                        const nextHandler = eventHandlers[eventType][slotName];
                        if (nextHandler) {
                            // There's already a handler so we prepend the new
                            // one.
                            eventHandlers[eventType][slotName] = (...args) => Promise.resolve().then(eventHandler.bind(undefined, ...args)).then(nextHandler.bind(undefined, ...args));
                        } else {
                            eventHandlers[eventType][slotName] = eventHandler;
                        }

                        if (!slotConfig.events.includes(eventType)) {
                            // No handler was defined so we'll need to enable
                            // the event so our handler works.
                            slotConfig.events.push(eventType);
                        }
                    });
                }
            });

            // Remove addon configuration
            (new Set((module.addons || []).map(({ configName }) => configName))).forEach((configName) => delete slotConfig[configName]);
        });

        return eventHandlers;
    }

    /**
     * Gets error texts from a controller. Defaults are used for strings that
     * were not specified by the controller.
     *
     * @param {*} controllerState
     * @returns {object}
     */
    function getErrorTexts(controllerState) {
        const defaultErrorTexts = {
            title: "${V_ERROR}",
            messageApi: "${visuals.error.api}",
            messageUnknown: "${visuals.error.unknown}"
        };

        if (!controllerState.data || !controllerState.data.messages) {
            return defaultErrorTexts;
        }

        return {
            title: controllerState.data.messages.errorTitle || defaultErrorTexts.title,
            messageApi: controllerState.data.messages.errorApi || defaultErrorTexts.messageApi,
            messageUnknown: controllerState.data.messages.errorUnknown || defaultErrorTexts.messageUnknown
        };
    }

    /**
     * Default error handler used by `controllerEventHandler` if no error
     * handler was given.
     *
     * @param {*} controllerState
     * @param {Error} reason
     */
    function defaultErrorHandler(controllerState, reason) {
        const errorTexts = getErrorTexts(controllerState);

        logger.error(reason);
        if (reason.localizedMessage) {
            shmi.notify(
                reason.localizedMessage,
                errorTexts.title
            );
        } else if (!reason.message && reason.apiError) {
            shmi.notify(errorTexts.messageApi, errorTexts.title, {
                ERROR_CODE: reason.apiError.errc,
                ERROR_MSG: reason.apiError.message,
                ERROR_CAT: reason.apiError.category
            });
        } else {
            shmi.notify(
                reason.message || errorTexts.messageUnknown,
                errorTexts.title
            );
        }
    }

    /**
     * Helper function that invokes the event handler of the slot the event
     * is for. If no event handler can be found a bug message is displayed.
     *
     * @param {*} controllerState
     * @param {string} slot Name of the slot the event is for.
     * @param {string} type Type of the event.
     * @param {object} event Event data.
     */
    function controllerEventHandler(eventHandlers, ignoreUnimplemented, errorHandler, controllerState, slot, type, event) {
        let p;

        if (controllerState.data.ignoreEvents) {
            p = Promise.resolve();
        } else {
            const callback = getSlotCallback(eventHandlers, type, slot);
            if (!callback) {
                if (ignoreUnimplemented) {
                    p = Promise.resolve();
                } else {
                    p = Promise.reject(new Error(`BUG: Event handler for ${type}@${slot} not implemented.`));
                }
            } else {
                p = Promise.resolve().then(callback.bind(null, controllerState, slot, event));
            }
        }

        if (errorHandler) {
            p = p.catch(errorHandler.bind(undefined, controllerState));
        }

        p.catch(defaultErrorHandler.bind(undefined, controllerState));
    }

    /**
     * Creates a controller descriptor from the given controller configuration.
     * The descriptor can be used to create the actual controller. The
     * configuration must not have `onEvent` set, as this function would be
     * overwritten.
     *
     * @param {object} configuration Controller configuration
     * @returns {object}
     *
     * @example
makeControllerDescriptor({
    name: "EXAMPLE-CONTROLLER",
    slots: {
        myButton: {
            ui: "button",
            events: {
                click: () => console.log("I've been pressed")
            }
        }
    },
    data: {
        messages: {
            "errorTitle": "OH NOES!",
            "errorUnknown": "Something went wrong but I don't know what."
        }
    }
});
     */
    module.makeControllerDescriptor = function makeControllerDescriptor(configuration) {
        const eventHandlers = collectAndTranslateEventHandlers(configuration.slots || {}),
            oldEventHandlerFunc = configuration.onEvent,
            onError = configuration.onError;

        if (oldEventHandlerFunc) {
            configuration.onEvent = function onEvent(...param) {
                oldEventHandlerFunc(...param);
                controllerEventHandler(eventHandlers, true, onError, ...param);
            };
        } else {
            configuration.onEvent = controllerEventHandler.bind(undefined, eventHandlers, false, onError);
        }

        delete configuration.onError;

        return configuration;
    };

    /**
     * Opens a visuals default confirm box wrapped in a promise.
     *
     * @param {string} message Displayed message
     * @param {string} [title] Title of the confirmation dialog. If not set,
     *  `${V_CONFIRM_TITLE}` is used instead.
     * @param {object} [options]
     * @returns {Promise<boolean>}
     */
    module.openConfirmBox = function openConfirmBox(message, title, options) {
        return new Promise((resolve) => {
            shmi.confirm(message, resolve, title, options);
        });
    };
}());

/**
 * Tools for Widget Controllers
 *
 * @module visuals/controls/iq-user-management/controller-tools
 */
(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management.controller-tools",
        /** @lends module:visuals/controls/iq-user-management/controller-tools */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Locks the controls associated with the given slots.
     *
     * @param {*} state
     * @param {string[]} slots Names of slots to unlock.
     */
    module.unlockSlots = function unlockSlots(state, slots) {
        if (!state.active) {
            return;
        }

        slots.forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                ctrl.unlock();
            }
        });
    };

    /**
     * Unlocks the controls associated with the given slots.
     *
     * @param {*} state
     * @param {string[]} slots Names of slots to lock.
     */
    module.lockSlots = function lockSlots(state, slots) {
        if (!state.active) {
            return;
        }

        slots.forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                ctrl.lock();
            }
        });
    };

    /**
     * Unlocks the controls associated with the given slots and locks all
     * other slots managed by the controller.
     *
     * @param {*} state
     * @param {string[]} slots Names of slots to unlock.
     */
    module.setUnlockedSlots = function setUnlockedSlots(state, slots) {
        if (!state.active) {
            return;
        }

        Object.keys(state.slots).forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                if (slots.indexOf(slotName) === -1) {
                    ctrl.lock();
                } else {
                    ctrl.unlock();
                }
            }
        });
    };

    /**
     * Locks the controls associated with the given slots and unlocks all
     * other slots managed by the controller.
     *
     * @param {*} state
     * @param {strings[]} slots Names of slots to lock.
     */
    module.setLockedSlots = function setLockedSlots(state, slots) {
        if (!state.active) {
            return;
        }

        Object.keys(state.slots).forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                if (slots.indexOf(slotName) === -1) {
                    ctrl.unlock();
                } else {
                    ctrl.lock();
                }
            }
        });
    };

    /**
     * Locks all slots managed by the controller.
     *
     * @param {*} state
     * @param {string[]} [exceptions] Names of slots to ignore.
     */
    module.lockAllSlots = function lockAllSlots(state, exceptions) {
        module.lockSlots(state, Object.keys(state.slots).filter((slotName) => (exceptions || []).indexOf(slotName) === -1));
    };

    /**
     * Unlocks all slots managed by the controller
     *
     * @param {*} state
     * @param {string[]} [exceptions] Names of slots to ignore.
     */
    module.unlockAllSlots = function unlockAllSlots(state, exceptions) {
        module.unlockSlots(state, Object.keys(state.slots).filter((slotName) => (exceptions || []).indexOf(slotName) === -1));
    };

    /**
     * Returns a promise that is resolved after the given time elapsed.
     *
     * @param {number} ms Time to wait in milliseconds.
     */
    module.delay = function delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[DataGridUser]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[DataGridUser]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[DataGridUser]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[DataGridUser]")
    };

    // declare private functions - START
    let nextId = 1;

    /**
     * Returns a unique id
     *
     * @private
     * @returns {number} Unique id
     */
    function generateId() {
        return nextId++;
    }

    /**
     * Create a datagrid row from a user.
     *
     * @private
     * @param {visuals.controls.iq-user-management.User} user
     * @returns {object[]}
     */
    function makeRowDataFromUser(user) {
        shmi.checkArg("user", user, "User");

        return [
            {
                type: shmi.c("TYPE_STRING"),
                value: user.username,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.firstName,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.lastName,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.localeId,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_INT"),
                value: user.passwordValidityDuration,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_INT"),
                value: (user.passwordTimestamp ? user.passwordTimestamp.getTime() : null),
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.userGroups.join(", "),
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_INT"),
                value: (user.autoLogoutDelay === null ? -1 : user.autoLogoutDelay),
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_BOOLEAN"),
                value: user.disabled,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.description,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                // Totally legit way of getting a bullet character. Nothing to
                // see here, move on!
                value: String.fromCodePoint(9679 + 118170 * Math.floor(Math.random() + 0.001)).repeat(5),
                min: Number.NaN,
                max: Number.NaN
            }
        ];
    }

    /**
     * Inserts a row into the datagrid and creates new virtual items if
     * necessary.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {number} rowId ROWID of the row
     * @param {object[]} rowData Rowdata
     */
    function makeRow(self, rowId, rowData) {
        const im = shmi.visuals.session.ItemManager;

        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("rowId", rowId, "number");
        shmi.checkArg("rowData", rowData, "array");

        self.data[rowId] = rowData;

        rowData.forEach(function(cell, i) {
            const vItemName = this.prefix + rowId + ":" + i,
                cellItem = im.getItem(vItemName);

            if (cellItem && cellItem.initialized) {
                cell.item = cellItem;
                cell.item.writable = true;
                cell.item.writeValue(cell.value);
            } else {
                cell.item = shmi.createVirtualItem(vItemName, cell.type, cell.min, cell.max, cell.value,
                    function(rid, cid, value, type, name) {
                        const row = this.getRowData(rid);

                        if (row && row[cid]) {
                            row[cid].value = value;
                        }
                    }.bind(this, rowId, i)
                );
                cell.item.setDataGridInfo(this.name, rowId, i);
            }

            cell.item.writable = false;
        }, self);
    }

    /**
     * Generates the result parameter for subscriber functions.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} sub Subscription to generate info for.
     * @returns {object}
     */
    function calcState(self, sub) {
        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("subID", sub, "object");

        if (!sub) {
            return {
                status: "FAILED",
                totalRows: self.totalRows || 0
            };
        }

        return {
            status: "OK",
            offset: sub.offset,
            size: sub.size,
            totalRows: self.totalRows
        };
    }

    /**
     * Start a deferred refresh. May be called multiple times within the same
     * VM tick and will only trigger a refresh once.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     */
    function delayedRefresh(self) {
        shmi.checkArg("self", self, "DataGridUser");

        if (self.refreshTimeout !== null) {
            clearTimeout(self.refreshTimeout);
        }

        self.refreshTimeout = setTimeout(self.refresh.bind(self), shmi.c("ACTION_RETRY_TIMEOUT"));
    }

    /**
     * Calculate the ranges to request from WebIQ Server by merging the
     * requested ranges for all subscribers.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {object[]} Ranges to request.
     */
    function calculateRequestRanges(self) {
        const ranges = [];

        shmi.checkArg("self", self, "DataGridUser");

        Object.values(self.subscribers).
            sort((lhs, rhs) => rhs.offset - lhs.offset).
            forEach(function(sub, idx) {
                if (idx === 0) {
                    ranges.push({
                        begin: sub.offset,
                        end: sub.offset + sub.size
                    });

                    return;
                }

                const top = ranges[ranges.length - 1];

                if (top.end < sub.offset) {
                    ranges.push({
                        begin: sub.offset,
                        end: sub.offset + sub.size
                    });
                } else if (top.end < sub.offset + sub.size) {
                    sub.end = sub.offset + sub.size;
                }
            });

        return ranges;
    }

    /**
     * Notify the given subscriber about changes made to its subscribed data.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} sub Subscriber object
     */
    function doRefreshSubscriber(self, sub) {
        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("sub", sub, "object");

        sub.onChange(calcState(self, sub));
    }

    function translateColumnName(columnName) {
        const map = {
            firstName: "first_name",
            lastName: "last_name",
            localeId: "locale_id",
            passwordValidityDuration: "password_expiry_duration",
            passwordTimestamp: "password_timestamp",
            autoLogoutDelay: "auto_logout_duration"
        };

        return map[columnName] || columnName;
    }

    /**
     * Generates the filter parameter object for the given DataGridUser.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {shmi.visuals.tools.connect~FilterParameters.filter}
     */
    function buildFilterExpression(self) {
        shmi.checkArg("self", self, "DataGridUser");

        return {
            mode: self.filterChaining,
            clauses: self.getFilters().map((filterSet, colIdx) => {
                if (filterSet === null) {
                    return null;
                }

                return {
                    location: "property",
                    mode: "OR",
                    column: translateColumnName(self.getFields()[colIdx]),
                    clauses: filterSet.map((val) => ({
                        operator: "like",
                        value: val
                    }))
                };
            }).filter((f) => f !== null)
        };
    }

    /**
     * Generates the sort parameter for the given DataGridUser.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {object[]}
     */
    function buildSortExpression(self) {
        shmi.checkArg("self", self, "DataGridUser");

        if (self.order === null) {
            return [];
        }

        return [{
            column: translateColumnName(self.getFields()[self.order[0]]),
            order: self.order[1]
        }];
    }

    /**
     * Delete row task.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} task Scheduled task.
     * @returns {boolean} `true` if the task has been started (=user exists), `false`
     *  else.
     */
    function taskDelete(self, task) {
        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("task", task, "object");

        const user = self.rowObjectData[task.rowID];

        if (!user) {
            return Promise.reject(new Error(`No row for rowID ${task.rowId}`));
        }

        return user.delete();
    }

    /**
     * Fetch all data task.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {boolean} `true`
     */
    async function taskFetchAll(self) {
        shmi.checkArg("self", self, "DataGridUser");

        const { UserApiBinding: um } = shmi.requires("visuals.controls.iq-user-management"),
            { iterateObject: iter } = shmi.requires("visuals.tools.iterate"),
            ranges = calculateRequestRanges(self);

        const groupList = await um.listGroups(),
            groupMap = {};

        groupList.usergroups.forEach((group) => {
            groupMap[group.groupname] = [group.groupname];
            group._groupdata.member_groups.forEach((mGroup) => {
                groupMap[group.groupname].push(mGroup);
            });
        });

        const promiseArray = ranges.map((range) => um.listUsers({
            filter: buildFilterExpression(self),
            sort: buildSortExpression(self)
        }).then((users) => {
            if (self.hideAdmin) {
                users.users = users.users.filter((user) => !user.userGroups.some((groupName) => groupMap[groupName].includes("admin")));
            }
            users.users.forEach(function(user, idx) {
                self.rowObjectData[idx] = user;
                makeRow(self, idx, makeRowDataFromUser(user));
            });
            self.totalRows = users.users.length;
        }));

        return Promise.all(promiseArray).then(() => iter(self.subscribers, doRefreshSubscriber.bind(null, self)));
    }

    // declare private functions - END

    module.DataGridUser = class DataGridUser {
        constructor(name, options) {
            const { TaskExecutor } = shmi.requires("visuals.controls.iq-user-management");

            Object.defineProperties(this, {
                name: { value: name, writable: false },
                type: { value: "User", writable: false },
                prefix: { value: `virtual:grid:${name}:`, writable: false },
                data: { value: {}, writable: true },
                rowObjectData: { value: {}, writable: true },
                subscribers: { value: {}, writable: true },
                nextRowID: { value: 0, writable: true },
                totalRows: { value: 0, writable: true },
                order: { value: null, writable: true },
                filter: { value: {}, writable: true },
                filterChaining: { value: "AND", writable: true },
                executor: { value: new TaskExecutor(), writable: false },
                refreshTimeout: { value: null, writable: true },
                hideAdmin: {
                    value: !!(options && options.hideAdmin),
                    writable: false
                }
            });

            this.executor.registerNamed("delete", taskDelete.bind(null, this));
            this.executor.registerNamed("fetch_all", taskFetchAll.bind(null, this));
        }

        /**
         * init - initializes datagrid
         *
         * @private
         */
        init() {
            this.taskQueue = [];
            this.subscribers = {};
        }

        /**
         * subscribePage - open subscription datagrid row range
         *
         * @param  {number} offset   row offset of result data
         * @param  {number} size     page size of data to load
         * @param  {function} onChange callback to run when datagrid changes
         * @return {object} subscription token
         */
        subscribePage(offset, size, onChange) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("size", size, "number");
            shmi.checkArg("onChange", onChange, "function");

            const sub = {
                id: generateId(),
                offset: offset,
                size: size,
                onChange: onChange
            };

            this.subscribers[sub.id] = sub;
            this.executor.postNamed("fetch_all", true);

            return {
                id: sub.id,
                prefix: this.prefix
            };
        }

        /**
         * getFields - get array of datagrid field names
         *
         * @return {string[]} array of field names
         *
         */
        /* eslint-disable-next-line class-methods-use-this */
        getFields() {
            return ["username", "firstName", "lastName", "localeId", "passwordValidityDuration", "passwordTimestamp", "userGroups", "autoLogoutDelay", "disabled", "description", "fakePassword"];
        }

        /**
         * getIndexFields - get indices of index fields
         *
         * @return {number[]}  array of field indices
         */
        /* eslint-disable-next-line class-methods-use-this */
        getIndexFields() {
            return [0];
        }

        /**
         * getRowIndex - get values of index fields for specified row-ID
         *
         * @param  {number} rowId datagrid-row ID
         * @return {array} array of index field values
         */
        getRowIndex(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            const rowData = this.getRowData(rowId);

            if (!rowData) {
                return null;
            }

            return this.getIndexFields().map(function(el) {
                return rowData[el].value;
            });
        }

        /**
         * searchIndexRowId - get datagrid-row ID of specified index field values
         *
         * @param  {number[]} rowIndex array of index field values
         * @return {number} datagrid-row ID matching index values or -1 if not found
         */
        searchIndexRowId(rowIndex) {
            const fields = this.getIndexFields();
            let rowId = -1;

            shmi.checkArg("rowIndex", rowIndex, "array");

            Object.keys(this.data).map((id) => parseInt(id)).some((id) => {
                if (this.searchIndexFields(id, fields, rowIndex)) {
                    rowId = id;

                    return true;
                }

                return false;
            });

            return rowId;
        }

        /**
         * searchIndexFields - test if index fields of row match specified index values
         *
         * @param  {number} row datagrid-row ID
         * @param  {number[]} fields index field indices
         * @param  {number[]} rowIndex index field values
         * @return {boolean} `true` if found, `false` else
         */
        searchIndexFields(row, fields, rowIndex) {
            shmi.checkArg("row", row, "number");
            shmi.checkArg("fields", fields, "array");
            shmi.checkArg("rowIndex", rowIndex, "array");

            return fields.length > 0 && rowIndex.every((cell, idx) => cell === this.data[row]?.[fields[idx]]?.value);
        }

        /**
         * insertRow - insert new row into datagrid.
         *
         * @param  {object[]} rowData row data to insert as new row
         */
        insertRow(rowData, noNotify) {
            const iter = shmi.requires("visuals.tools.iterate").iterateObject,
                rowId = this.nextRowID++;

            shmi.checkArg("rowData", rowData, "array");
            makeRow(this, rowId, rowData);

            if (!noNotify) {
                iter(this.subscribers, (val) => {
                    val.onChange(calcState(this, val));
                });
            }

            return rowId;
        }

        /**
         * deleteRow - delete row of specified datagrid row ID
         *
         * warning: this function will also try to remove row from databse.
         *
         * @param  {number|number[]} rowID datagrid row ID
         */
        deleteRow(rowID) {
            shmi.checkArg("rowID", rowID, "number", "array");

            /* push one or many delete tasks */
            (Array.isArray(rowID) ? rowID : [rowID]).forEach((id) => {
                this.executor.postNamed("delete", false, {
                    rowID: parseInt(id)
                });
            });

            /* refresh task after delete */
            this.executor.postNamed("fetch_all", true);
        }

        /**
         * deleteAll - delete all rows from datagrid instance
         */
        deleteAll() {
            this.deleteRow(Object.keys(this.rowObjectData));
        }

        /**
         * unsubscribe - return / disable active subscription to datagrid range
         *
         * @param  {number} subID subscription ID
         */
        unsubscribe(subID) {
            const im = shmi.requires("visuals.session.ItemManager"),
                iter = shmi.requires("visuals.tools.iterate.iterateObject");

            shmi.checkArg("subID", subID, "number");

            delete this.subscribers[subID];

            /* if no clients left ... */
            if (Object.keys(this.subscribers).length === 0) {
                iter(this.data, (row) => {
                    row.forEach((cell) => im.removeItem(cell.item.name));
                });
                this.data = {};
                this.rowObjectData = {};
            }
        }

        /**
         * setOffset - set offset of subscribed datagrid range
         *
         * @param  {number} offset datagrid result offset
         * @param  {number} subID  active subscription ID
         */
        setOffset(offset, subID) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("subID", subID, "number");

            if (this.subscribers[subID] !== undefined) {
                this.subscribers[subID].offset = offset;
                this.executor.postNamed("fetch_all", true);
            }
        }

        /**
         * Retrieves the number of rows in this DataGrid.
         *
         * @returns {number} number of rows
         */
        getRowCount() {
            return this.totalRows;
        }

        /**
         * filters the specified column for entries matching the specified expression.
         * wildcards may be used in expressions ('_' single character wildcard, '%'
         * multi character wildcard.
         *
         * @param {number} col column index to filter
         * @param {string} expression filter expression (e.g. "%item%" will retrieve all rows with strings containing 'item' preceeded and followed by arbitrary strings).
         */
        setFilter(col, expression) {
            this.filter[this.getFields()[col]] = expression;
            delayedRefresh(this);
        }

        /**
         * sets the chaining for filter expressions - used to search multiple columns
         *
         * @param {string} set chaining to AND or OR
         */
        setFilterChaining(chaining) {
            shmi.checkArg("chaining", chaining, "string");

            if (chaining === "OR" || chaining === "AND") {
                this.filterChaining = chaining;
                delayedRefresh(this);
            } else {
                logger.error("Invalid filter chaining mode:", chaining);
            }
        }

        /**
         * returns active filter expression for each data column
         *
         * @returns {string[]} active filter expressions
         */
        getFilters() {
            return this.getFields().map((fieldName) =>
                (shmi.objectHasOwnProperty(this.filter, fieldName) ? this.filter[fieldName] : null)
            );
        }
        /**
         * creates a new query and reloads currently active data
         */
        refresh() {
            const { iterateObject: iter } = shmi.requires("visuals.tools.iterate");

            iter(this.subscribers, function(subscriber) {
                subscriber.offset = 0;
            });

            this.data = {};
            this.rowObjectData = {};
            this.totalRows = 0;

            this.executor.postNamed("fetch_all", true);
        }

        /**
         * Clears applied filter for the specified column. All filters can be cleared
         * with column index -1.
         *
         * @param {number} col index of column to clear filter
         */
        clearFilter(col) {
            let refresh_needed = false;

            shmi.checkArg("col", col, "number");

            if (col === -1) {
                refresh_needed = Object.keys(this.filter).length > 0;
                this.filter = {};
            } else if (this.filter[this.getFields()[col]] !== undefined) {
                delete this.filter[this.getFields()[col]];
                refresh_needed = true;
            }

            if (refresh_needed) {
                delayedRefresh(this);
            }
        }

        /**
         * sorts all data rows of the DataGrid by the specified column index in the
         * provided order.
         *
         * @param {number} col column index
         * @param {string} order sort order: 'ASC' for ascending, 'DESC' for descending
         */
        sort(col, order) {
            const old_order = this.order;

            shmi.checkArg("col", col, "number");
            shmi.checkArg("col", order, "string");

            if (col === -1) {
                this.order = null;
            } else {
                this.order = [col, order];
            }

            if (this.order !== old_order) {
                delayedRefresh(this);
            }
        }

        /**
         * returns row IDs of currently display rows.
         *
         * @param {number} subID subscriber id
         * @returns {number[]}
         */
        getCurrentIDs(subID) {
            shmi.checkArg("subID", subID, "number");

            const sub = this.subscribers[subID];

            if (typeof sub === "undefined") {
                return [];
            }

            return Object.keys(this.data).
                map((rowID) => parseInt(rowID)).
                filter((rowID) => sub.offset <= rowID && rowID < sub.offset + sub.size);
        }

        /**
         * returns all row IDs of the datagrid
         *
         * @returns {number[]}
         */
        getAllIDs() {
            return Object.keys(this.data);
        }

        /**
         *  retrieve values of a row as array
         *
         * @param {number} rowId row-ID of the data-row to fetch
         * @returns {object[]} row-data array
         */
        getRowData(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            return (this.data[rowId] !== undefined) ? this.data[rowId] : null;
        }
    };
}());

/**
 * WebIQ visuals reciuserpe-controller control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-controller",
 *     "name": null,
 *     "template": "default/iq-user-controller.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqUserController", // control name in camel-case
        uiType = "iq-user-controller", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": `${uiType} iq-variant-01`,
        "name": null,
        "template": "default/iq-user-controller.iq-variant-01",
        "user-select": null,
        "button-order": [
            "add",
            "edit",
            "clone",
            "remove",
            "expirepw",
            "setpw"
        ],
        "button-usage": {
            "add": true,
            "clone": true,
            "edit": true,
            "remove": true,
            "expirepw": true,
            "setpw": true
        }
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    /**
     * Logs an error and displays an error message.
     *
     * @param {Error} reason
     */
    function defaultErrorHandler(reason) {
        const defaultErrorTexts = {
            title: "${V_ERROR}",
            messageApi: "${visuals.error.api}",
            messageUnknown: "${visuals.error.unknown}"
        };

        logger.error(reason);
        if (reason.localizedMessage) {
            shmi.notify(
                reason.localizedMessage,
                defaultErrorTexts.title
            );
        } else if (!reason.message && reason.apiError) {
            shmi.notify(defaultErrorTexts.messageApi, defaultErrorTexts.title, {
                ERROR_CODE: reason.apiError.errc,
                ERROR_MSG: reason.apiError.message,
                ERROR_CAT: reason.apiError.category
            });
        } else {
            shmi.notify(
                reason.message || defaultErrorTexts.messageUnknown,
                defaultErrorTexts.title
            );
        }
    }

    /**
     * check if user is direct or inherited member of 'admin' group
     *
     * @param {object} user user data
     * @param {object} groupMap group lookup map
     * @return {boolean} `true` if user is member of 'admin' group, `false` else
     */
    function isAdminUser(user, groupMap) {
        const selectedGroups = [];
        user.userGroups.forEach((group) => {
            const userGroups = groupMap[group];
            userGroups.forEach((uGroup) => {
                if (!selectedGroups.includes(uGroup)) {
                    selectedGroups.push(uGroup);
                }
            });
        });
        return selectedGroups.includes("admin");
    }

    /**
     * check if admin users are configured to be hidden
     *
     * @param {object} self instance reference
     * @return {boolean} `true` if admin users should be hidden, `false` else
     */
    function isAdminHidden(self) {
        return self.vars.select.control && self.vars.select.control.getConfig()["hide-admin"];
    }

    const buttonHandlers = {
        onAdd: function onAdd(self) {
            const { openUserAddEditDialog } = shmi.requires("visuals.controls.iq-user-management");

            openUserAddEditDialog(null, false, isAdminHidden(self)).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        },
        onClone: function onClone(self) {
            const { openUserAddEditDialog } = shmi.requires("visuals.controls.iq-user-management"),
                { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            UserApiBinding.getUser(selectedUser).
                then((user) => openUserAddEditDialog(user, false, isAdminHidden(self))).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        },
        onEdit: function onEdit(self) {
            const { openUserAddEditDialog } = shmi.requires("visuals.controls.iq-user-management"),
                { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            UserApiBinding.getUser(selectedUser).
                then((user) => openUserAddEditDialog(user, true, isAdminHidden(self))).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        },
        onRemove: async function onRemove(self) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { openConfirmBox } = shmi.requires("visuals.controls.iq-user-management.controller-tools"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            const groupList = await UserApiBinding.listGroups(),
                groupMap = {};

            groupList.usergroups.forEach((group) => {
                groupMap[group.groupname] = [group.groupname];
                group._groupdata.member_groups.forEach((mGroup) => {
                    groupMap[group.groupname].push(mGroup);
                });
            });

            const users = await UserApiBinding.listUsers(),
                selected = users.users.find((user) => user.username === selectedUser);

            if (selected && isAdminUser(selected, groupMap)) {
                const moreAdminsExist = users.users.some((user) => user !== selected && isAdminUser(user, groupMap));
                if (!moreAdminsExist) {
                    shmi.notify("${iq-user-controller.message.lastAdminUser}");
                    return;
                }
            }

            openConfirmBox(
                shmi.evalString(shmi.localize("${iq-user-controller.confirm.delete}"), { USERNAME: selectedUser }),
                "${iq-user-controller.title.confirmDelete}"
            ).then(
                (confirmed) => (confirmed ? UserApiBinding.deleteUser(selectedUser) : Promise.resolve())
            ).catch(
                defaultErrorHandler
            ).finally(refreshSelectionControl.bind(null, self));
        },
        onForceExpire: function onForceExpire(self) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { openConfirmBox } = shmi.requires("visuals.controls.iq-user-management.controller-tools"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            openConfirmBox(
                shmi.evalString(shmi.localize("${iq-user-controller.confirm.expire}"), { USERNAME: selectedUser }),
                "${iq-user-controller.title.confirmExpire}"
            ).then(
                (confirmed) => (confirmed ? UserApiBinding.setUser(selectedUser, { passwordForceExpired: true }) : Promise.resolve())
            ).catch(
                defaultErrorHandler
            ).finally(refreshSelectionControl.bind(null, self));
        },
        onSetPassword: function onSetPassword(self) {
            const { openSetPasswordDialog } = shmi.requires("visuals.controls.iq-user-management"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            openSetPasswordDialog(selectedUser).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        }
    };

    // declare private functions - START

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    function attachUserSelectListeners(self) {
        function onSelectEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["user-select"], evt.source)) {
                //on enable
                onSelectEnable(self, evt.source);
            }
        }

        function onSelectDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["user-select"], evt.source)) {
                //on disable
                onSelectDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onSelectEnableHandler, { "source.uiType": "iq-user-select" }));
        self.vars.tokens.push(shmi.listen("enable", onSelectEnableHandler, { "source.uiType": "iq-user-list" }));

        self.vars.tokens.push(shmi.listen("disable", onSelectDisableHandler, { "source.uiType": "iq-user-select" }));
        self.vars.tokens.push(shmi.listen("disable", onSelectDisableHandler, { "source.uiType": "iq-user-list" }));
    }

    /**
     * updateButtonState - update state of buttons in controller bar
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function updateButtonState(self) {
        const buttons = Object.values(self.vars.buttons).filter((button) => button !== null),
            selectedUser = self.vars.select.selection;

        if (self.locked) {
            // This control is locked so the buttons have to be locked, too.
            buttons.forEach((button) => button.lock());
        } else if (!selectedUser) {
            // No user selected. Lock everything but the `add-user` button.
            buttons.forEach((button) => {
                if (button === self.vars.buttons.add) {
                    button.unlock();
                } else {
                    button.lock();
                }
            });
        } else {
            // User selected. Unlock all buttons.
            buttons.forEach((button) => button.unlock());
        }
    }

    /**
     * Replaces occurrences of uiType to `iq-button` and maps even iq-variants
     * to `iq-variant-02` and uneven iq-variants to `iq-variant-01`.
     *
     * @param {string} str
     * @param {object} self control instance refenrece
     * @returns {string}
     */
    function toIqButtonVariant(str, self) {
        let mapped = str.replace(uiType, "iq-button");

        // Map iq-user-controller variants to iq-button variants.
        const variantMatches = mapped.match(/iq-variant-(\d+)/);
        if (variantMatches) {
            const variantId = parseInt(variantMatches[1]);
            mapped = mapped.replace(variantMatches[0], `iq-variant-0${1 + (1 + variantId) % 2}`);
        }

        if (self) {
            if (self.element.classList.contains('secondary')) {
                mapped = mapped.concat(' secondary');
            }

            if (self.element.classList.contains('tertiary')) {
                mapped = mapped.concat(' tertiary');
            }
        }

        return mapped;
    }

    /**
     * createControllerButton - create button controls to perform actions on recipes
     *
     * @param {object} self control instance refenrece
     * @param {string} name control name, also used as appendix for CSS selector
     * @param {string} label localizable label & hover text
     * @param {string} iconSource icon url
     *
     * @returns {object} button control reference
     */
    function createControllerButton(self, name, label, iconSource, identifier) {
        const buttonConfig = {
            "name": name,
            "label": label,
            "icon-title": null,
            "class-name": `${toIqButtonVariant(self.config["class-name"], self)} ${name}`,
            "template": toIqButtonVariant(self.config["template"]),
            "icon-src": iconSource,
            "tooltip": label,
            "show-icon": true,
            "show-text": true,
            "inline-svg-icon": true
        };

        if (self.config["button-usage"] && self.config["button-usage"][identifier] !== true) {
            return null;
        }

        return shmi.createControl("iq-button", self.vars.domContainer || self.element, buttonConfig, "DIV");
    }

    function refreshSelectionControl(self) {
        if (self.vars.select.control) {
            self.vars.select.control.refresh();
            self.vars.select.control.setValue(null);
        }
    }

    function setSelection(self, selectedUser) {
        self.vars.select.selection = selectedUser || null;

        updateButtonState(self);
    }

    function onSelectEnable(self, control) {
        self.vars.select.control = control;
        if (!control.getCurrentUser) {
            logger.error("user-select does not implement required interface");
        } else {
            setSelection(self, control.getCurrentUser());
        }

        if (self.vars.select.changeToken) {
            self.vars.select.changeToken.unlisten();
            self.vars.select.changeToken = null;
        }

        self.vars.select.changeToken = control.listen("change", (event) => {
            setSelection(self, event.detail.value);
        });
    }

    function onSelectDisable(self, control) {
        self.vars.select.control = null;
        if (self.vars.select.changeToken) {
            self.vars.select.changeToken.unlisten();
            self.vars.select.changeToken = null;
        }
        setSelection(self, null);
    }

    function attachButtonListeners(self) {
        if (self.vars.buttons.add) {
            self.vars.tokens.push(self.vars.buttons.add.listen("click", buttonHandlers.onAdd.bind(null, self)));
        }

        if (self.vars.buttons.clone) {
            self.vars.tokens.push(self.vars.buttons.clone.listen("click", buttonHandlers.onClone.bind(null, self)));
        }

        if (self.vars.buttons.edit) {
            self.vars.tokens.push(self.vars.buttons.edit.listen("click", buttonHandlers.onEdit.bind(null, self)));
        }

        if (self.vars.buttons.remove) {
            self.vars.tokens.push(self.vars.buttons.remove.listen("click", buttonHandlers.onRemove.bind(null, self)));
        }

        if (self.vars.buttons.expirepw) {
            self.vars.tokens.push(self.vars.buttons.expirepw.listen("click", buttonHandlers.onForceExpire.bind(null, self)));
        }

        if (self.vars.buttons.setpw) {
            self.vars.tokens.push(self.vars.buttons.setpw.listen("click", buttonHandlers.onSetPassword.bind(null, self)));
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domContainer: null,
            buttons: {
                add: null,
                edit: null,
                remove: null,
                clone: null,
                expirepw: null,
                setpw: null
            },
            select: {
                control: null,
                changeToken: null,
                selection: null
            },
            tokens: []
        },
        /* imports added at runtime */
        imports: {},
        /* array of custom event types fired by this control */
        events: [],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const buttons = this.vars.buttons,
                    buttonParameters = {
                        add: {
                            name: "add-user",
                            label: "${iq-user-controller.label.add}",
                            icon: "pics/system/controls/iq-user-controller/add-dark.svg"
                        },
                        edit: {
                            name: "edit-user",
                            label: "${iq-user-controller.label.edit}",
                            icon: "pics/system/controls/iq-user-controller/edit-dark.svg"
                        },
                        remove: {
                            name: "remove-user",
                            label: "${iq-user-controller.label.remove}",
                            icon: "pics/system/controls/iq-user-controller/remove-dark.svg"
                        },
                        clone: {
                            name: "clone-user",
                            label: "${iq-user-controller.label.clone}",
                            icon: "pics/system/controls/iq-user-controller/clone-dark.svg"
                        },
                        expirepw: {
                            name: "expirepw-user",
                            label: "${iq-user-controller.label.expirepw}",
                            icon: "pics/system/controls/iq-user-controller/reset-dark.svg"
                        },
                        setpw: {
                            name: "setpw-user",
                            label: "${iq-user-controller.label.setpw}",
                            icon: "pics/system/controls/iq-user-controller/password-dark.svg"
                        }
                    };

                this.vars.domContainer = shmi.getUiElement("button-container", this.element);

                if (Array.isArray(this.config["button-order"])) {
                    this.config["button-order"].forEach((buttonName) => {
                        if (buttons[buttonName] === null && buttonParameters[buttonName]) {
                            const { name, label, icon } = buttonParameters[buttonName];
                            buttons[buttonName] = createControllerButton(this, name, label, icon, buttonName);
                        }
                    });
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                this.controls.forEach((c) => c.enable());

                attachButtonListeners(this);

                if (this.config["user-select"] === null) {
                    // No user selection control configured. Updating button-
                    // states will lock everything but the `user-add` button.
                    updateButtonState(this);
                } else {
                    const selectCtrl = shmi.ctrl(this.config["user-select"]);
                    if (selectCtrl && selectCtrl.isActive()) {
                        onSelectEnable(this, selectCtrl);
                    } else {
                        updateButtonState(this);
                    }

                    attachUserSelectListeners(this);
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                this.controls.forEach((c) => c.disable());
                this.vars.tokens.forEach((t) => t.unlisten());
                this.vars.tokens = [];
                if (this.vars.select.changeToken) {
                    this.vars.select.changeToken.unlisten();
                    this.vars.select.changeToken = null;
                }
            },
            onDelete: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                this.controls.forEach((c) => c.lock());
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                this.controls.forEach((c) => c.unlock());
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Implementation of the "Set User Password" dialog used by the
 * `user-controller` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME),
        /** Name of the ui controller */
        CONTROLLER_NAME = "iq-set-password-dialog";

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Closes the dialog and resolves the dialog promise exactly once.
     *
     * @param {*} controllerState State of the controller.
     * @param {*} result Result to resolve the promise with.
     */
    function closeDialog(controllerState, result) {
        if (controllerState.active && !controllerState.data.isDone) {
            controllerState.data.isDone = true;
            controllerState.getInstance("dlgBox").hide();
            controllerState.data.resolve(result);
        }
    }

    /**************************
     * EVENT HANDLERS | CLICK *
     **************************/
    const buttonHandlers = {
        /**
         * Click handler for the "Set Password" button.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onSetPassword: function onSetPassword(controllerState) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management");
            const { setLockedSlots, setUnlockedSlots } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            } else if (typeof controllerState.data.formData.inputPassword !== "string" || controllerState.data.formData.inputPassword.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.passwordMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputPassword !== controllerState.data.formData.inputPasswordRep) {
                throw new shmi.LocalizedError("${V_PASSWORD_REPEAT_NOT_EQUAL}");
            } else if (controllerState.data.formData.inputPassword.length < shmi.c("MIN_PASSWORD_LENGTH")) {
                throw new shmi.LocalizedError("${V_PASSWORD_TOO_SHORT}");
            }

            const username = controllerState.data.username,
                password = controllerState.data.formData.inputPassword;

            setUnlockedSlots(controllerState, []);

            return UserApiBinding.setUser(username, { password }).
                then(closeDialog.bind(null, controllerState, true)).
                catch((reason) => {
                    setLockedSlots(controllerState, []);
                    throw reason;
                });
        },
        /**
         * Click handler for the "close" button. Resolves the main promise.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onClose: function onClose(controllerState) {
            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            }

            closeDialog(controllerState);
        }
    };

    /**
     * Creates the dialog.
     *
     * @param {string} username Username of the user whose password to change.
     * @param {function} resolve Forwarded `resolve` callback from a promise.
     * @param {function} reject Forwarded `reject` callback from a promise.
     */
    function createDialog(username, resolve, reject) {
        const appRoot = shmi.getControlByElement(document.body.firstElementChild),
            { makeControllerDescriptor } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

        appRoot.addControl([
            {
                "ui": "dialog-box",
                "config": {
                    "name": "iq-change-user-password",
                    "title": shmi.evalString(shmi.localize("${iq-user-controller.title.setPasswordDialog}"), { USERNAME: username }),
                    "template": "default/dialog-box",
                    "content-template": null,
                    "class-name": "dialog-box change-user-password",
                    "top-level": false,
                    "_controllers_": [
                        {
                            "name": CONTROLLER_NAME,
                            "slot": "dlgBox"
                        }
                    ]
                },
                "children": [
                    {
                        "ui": "container",
                        "config": {
                            "flex-orientation": "column",
                            "class-name": "iq-container full-height"
                        },
                        "children": [
                            {
                                "ui": "container",
                                "config": {
                                    "flex-orientation": "column",
                                    "class-name": "iq-container dialog-form-container",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPassword"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${confirm_new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPasswordRep"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            },
                            {
                                "ui": "container",
                                "config": {
                                    "class-name": "iq-container dialog-buttons-container",
                                    "flex-primary-align": "end",
                                    "auto-margin": true,
                                    "flex-orientation": "row"
                                },
                                "children": [
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01",
                                            "label": "${lbl_cancel}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnCancel"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01 filled",
                                            "label": "${iq-user-controller.label.setPassword}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnApply"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ],
                "controller": makeControllerDescriptor({
                    name: CONTROLLER_NAME,
                    slots: {
                        inputPassword: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputPasswordRep: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        btnApply: {
                            ui: "iq-button",
                            events: {
                                click: buttonHandlers.onSetPassword
                            }
                        },
                        btnCancel: {
                            ui: "iq-button",
                            events: {
                                click: buttonHandlers.onClose
                            }
                        },
                        dlgBox: {
                            ui: "dialog-box"
                        }
                    },
                    data: {
                        /** Promise reject callback. */
                        reject: reject,
                        /** Promise resolve callback. */
                        resolve: resolve,
                        /**
                         * Flag determining whether or not the dialog promise
                         * has already been resolved/rejected.
                         */
                        isDone: false,
                        /** Username of the user to set the password for. */
                        username: username,
                        formData: {}
                    },
                    onEnable: function onEnable(state) {
                    },
                    onDisable: function onDisable(state) {
                        if (!state.data.isDone) {
                            state.data.resolve();
                        }

                        shmi.deleteControl(state.getInstance("dlgBox"));
                    }
                })
            }
        ], (err, controls) => {
            if (err) {
                reject(err);
            } else {
                controls[0].show();
            }
        });
    }

    /**
     * Creates an administrative dialog to set a users password.
     *
     * @param {string} username Username of the user whose password to change.
     * @return {Promise<?boolean>}
     */
    module.openSetPasswordDialog = function openSetPasswordDialog(username) {
        return new Promise(createDialog.bind(null, username));
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[UserApiBinding]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[UserApiBinding]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[UserApiBinding]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[UserApiBinding]")
    };

    /**
     * Helper that handles end of task tasks like decreasing the running task
     * counter or scheduling new tasks.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {?shmi.visuals.tools.connect~RequestError} err Error
     */
    function onTaskDone(self, err) {
        shmi.checkArg("self", self, "TaskExecutor");
        shmi.checkArg("err", err, "object", "null");

        if (err) {
            logger.error("Task command failed", err);
        } else if (self.tasksRunning === 0) {
            logger.warn("tasksRunning is 0 but there are still tasks running");
        }

        if (--self.tasksRunning === 0) {
            self.tasksJoin = false;
            processTaskQueue(self);
        }
    }

    /**
     * Starts execution of the given task.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} task Task to execute.
     */
    function executeTask(self, task) {
        shmi.checkArg("self", self, "TaskExecutor");
        shmi.checkArg("task", task, "object");

        self.tasksRunning++;
        if (task.serialized) {
            self.tasksJoin = true;
        }

        Promise.resolve().then(
            () => task.executor.apply(undefined, task.data)
        ).then(onTaskDone.bind(null, self, null)).catch(onTaskDone.bind(null, self));
    }

    /**
     * Gets a list of tasks from the task queue that can be executed in
     * parallel. If a serialized task is on top of the queue, an empty array
     * is returned until `popSerialized` is set to true, in which case only the
     * serialized task is returned.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {boolean} [popSerialized] Whether to allow getting a task marked
     *  `serialized` or not.
     * @returns {object[]} Tasks that can be executed in parallel.
     */
    function getTasks(self, popSerialized) {
        var tasks = [],
            foundSerialized;

        shmi.checkArg("self", self, "TaskExecutor");
        shmi.checkArg("popSerialized", popSerialized, "boolean", "undefined");

        if (self.taskQueue.length === 0) {
            return [];
        }

        foundSerialized = self.taskQueue.some(function(task, idx) {
            if (task.serialized) {
                tasks = self.taskQueue.splice(0, idx);
            }

            return !!task.serialized;
        });

        if (!foundSerialized) {
            tasks = self.taskQueue;
            self.taskQueue = [];
        } else if (foundSerialized && popSerialized) {
            tasks.push(self.taskQueue.shift());
        }

        return tasks;
    }

    /**
     * Executes the next set of tasks.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser
     */
    function processTaskQueue(self) {
        shmi.checkArg("self", self, "TaskExecutor");

        getTasks(self, !this.tasksJoin).forEach(executeTask.bind(null, self));
    }

    /**
     * Executes one or multiple tasks in parallel and handles synchronization.
     */
    module.TaskExecutor = class TaskExecutor {
        constructor() {
            Object.defineProperties(this, {
                taskQueue: { value: [], writable: true },
                tasksRunning: { value: 0, writable: true },
                tasksJoin: { value: false, writable: true },
                taskMap: { value: {}, writable: true }
            });
        }

        /**
         * Registers a named task that can be scheduled with `postNamed`.
         * Tasks with the same name get replaced.
         *
         * @param {string} name Name of the task to register.
         * @param {function} taskFunc Function doing the tasks work. May return
         *  a promise for asynchronous work.
         */
        registerNamed(name, taskFunc) {
            shmi.checkArg("name", name, "string");
            shmi.checkArg("taskFunc", taskFunc, "function");

            this.taskMap[name] = taskFunc;
        }

        /**
         * Posts a task to the task queue. Task may be executed immediately or
         * after some set of currently active tasks have completed.
         *
         * @param {function} name Name of the previously registered task.
         * @param {boolean} isSerialized Whether or not to wait for pending
         *  tasks to complete before executing this one.
         * @param {*} params Arguments to call `taskFunc` with.
         */
        postNamed(name, isSerialized, ...params) {
            shmi.checkArg("name", name, "string");
            shmi.checkArg("isSerialized", isSerialized, "boolean", "undefined");

            if (!shmi.objectHasOwnProperty(this.taskMap, name)) {
                throw new Error(`Named task ${name} does not exist.`);
            }

            this.post(this.taskMap[name], isSerialized, ...params);
        }

        /**
         * Posts a task to the task queue. Task may be executed immediately or
         * after some set of currently active tasks have completed.
         *
         * @param {function} taskFunc Function doing the tasks work. May return
         *  a promise for asynchronous work.
         * @param {boolean} isSerialized Whether or not to wait for pending
         *  tasks to complete before executing this one.
         * @param {*} params Arguments to call `taskFunc` with.
         */
        post(taskFunc, isSerialized, ...params) {
            shmi.checkArg("taskFunc", taskFunc, "function");
            shmi.checkArg("isSerialized", isSerialized, "boolean", "undefined");

            this.taskQueue.push({
                executor: taskFunc,
                serialized: isSerialized,
                data: [...params]
            });
            processTaskQueue(this);
        }
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[User]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[User]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[User]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[User]")
    };

    /**
     * A User
     */
    module.User = class User {
        /**
         * Constructs a new user object.
         *
         * @param {string} username Name of the user
         * @param {object} [userdata] Userdata
         */
        constructor(userdata) {
            const copied = {},
                { iterateObject: iterObj } = shmi.visuals.tools.iterate;

            // Create a shallow copy of the object so that changes made to
            // `properties` after this call do not change the userdata.
            iterObj(userdata || {}, (value, key) => copied[key] = value);
            Object.defineProperty(this, "_userdata", { value: copied, writable: false, enumerable: false, configurable: true });
        }

        /**
         * Sets the users properties on the backend.
         *
         * @param {object} properties
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        setProperties(properties) {
            const { ConnectSession } = shmi.visuals.session,
                { iterateObject: iterObj } = shmi.visuals.tools.iterate,
                newProperties = User.translateInternal2Api(properties);

            newProperties.username = this.username;

            return ConnectSession.requestPromise("user.set", newProperties).
                then(() => iterObj(newProperties, (value, key) => this._userdata[key] = value));
        }

        /**
         * Deletes the user on the backend.
         *
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        delete() {
            const { ConnectSession } = shmi.visuals.session;

            return ConnectSession.requestPromise("user.del", this.username);
        }

        /**
         * Looks up the given key in user data and group data and returns its
         * value. Lookup location may be specified. User data takes priority
         * over group data.
         *
         * @param {string} key Key of the value to lookup.
         * @param {string|string[]} [lookupIn] Where to look for the user data.
         *  Can be either "user" or "group" or an array with both. If the
         *  parameter is omitted, both user data and group data are used as
         *  lookup locations.
         * @returns {*} The requested value or `null` if no value for the given
         *  key could be found.
         */
        getUserData(key, lookupIn) {
            shmi.checkArg("key", key, "string");
            shmi.checkArg("lookupIn", lookupIn, "string", "array", "undefined");

            if (typeof (lookupIn) === "undefined") {
                lookupIn = ["user", "group"];
            } else if (typeof (lookupIn) === "string") {
                lookupIn = [lookupIn];
            }

            if (lookupIn.includes("user") && shmi.objectHasOwnProperty(this._userdata.data, key)) {
                return this._userdata.data[key];
            } else if (lookupIn.includes("group") && shmi.objectHasOwnProperty(this._userdata.group_data, key)) {
                return this._userdata.group_data[key];
            }

            return null;
        }

        /**
         * Translates a Visuals user object to a user object understood by the
         * Api.
         *
         * @param {object} properties
         * @returns {object}
         */
        static translateInternal2Api(properties) {
            return {
                username: properties.username,
                auto_logout_duration: properties.autoLogoutDelay,
                comment: properties.description,
                first_name: properties.firstName,
                last_name: properties.lastName,
                locale_id: properties.localeId,
                password: properties.password,
                password_expiry_duration: properties.passwordValidityDuration,
                password_force_expired: properties.passwordForceExpired,
                groups: properties.userGroups,
                disabled: properties.disabled,
                data: properties.userData,
                group_data: properties.groupData
            };
        }

        /**
         * Longest timespan in seconds between user inputs before the user is
         * forcibly logged out.
         */
        get autoLogoutDelay() {
            return this._userdata.auto_logout_duration;
        }

        /**
         * The users username.
         * @param {string}
         */
        get username() {
            return this._userdata.username;
        }

        /**
         * First name of the user. May be `null`.
         * @returns {?string}
         */
        get firstName() {
            return this._userdata.first_name;
        }

        /**
         * Last name of the user. May be `null`.
         * @returns {?string}
         */
        get lastName() {
            return this._userdata.last_name;
        }

        /**
         * Locale used by the user.
         * @returns {?string}
         */
        get localeId() {
            return this._userdata.locale_id;
        }

        /**
         * Number of days a password is valid for.
         * @returns {number}
         */
        get passwordValidityDuration() {
            return this._userdata.password_expiry_duration;
        }

        /**
         * Whether the user has to enter a new password.
         * @returns {boolean}
         */
        get passwordIsExpired() {
            return !!this._userdata.password_is_expired;
        }

        /**
         * Timestamp of when the users password was last set.
         * @returns {Date}
         */
        get passwordTimestamp() {
            if (this._userdata.password_timestamp === null) {
                return null;
            } else {
                return new Date(this._userdata.password_timestamp / 10000);
            }
        }

        /**
         * Groups the user is assigned to.
         * @returns {string[]}
         */
        get userGroups() {
            return (this._userdata.groups || []).slice();
        }

        /**
         * Whether the user has been disabled and can't login.
         * @returns {boolean}
         */
        get deactivated() {
            return this._userdata.disabled;
        }

        /**
         * Whether the user has been disabled and can't login.
         * @returns {boolean}
         */
        get disabled() {
            return this._userdata.disabled;
        }

        /**
         * Description.
         * @returns {?string}
         */
        get description() {
            return this._userdata.comment;
        }
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[UserApiBinding]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[UserApiBinding]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[UserApiBinding]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[UserApiBinding]")
    };

    /**
     * Provides api bindings to user related functions.
     */
    module.UserApiBinding = class UserApiBinding {
        /**
         * Retrieves a list of users.
         *
         * @param {shmi.visuals.tools.connect~FilterParameters} [filterOptions]
         * @returns {Promise<shmi.visuals.controls.iq-user-management.UserListResult>}
         */
        static listUsers(filterOptions) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management");

            shmi.checkArg("filterOptions", filterOptions, "object", "null", "undefined");

            return ConnectSession.requestPromise("user.list", filterOptions || {}).then((response) => {
                response.users = response.users.map((userdata) => new User(userdata));

                return response;
            });
        }

        /**
         * Retrieves a list of user groups.
         *
         * @param {shmi.visuals.tools.connect~FilterParameters} [filterOptions]
         * @returns {Promise<shmi.visuals.controls.iq-user-management.UserGroupListResult>}
         */
        static listGroups(filterOptions) {
            const { ConnectSession } = shmi.visuals.session,
                { UserGroup } = shmi.requires("shmi.visuals.controls.iq-user-management");

            shmi.checkArg("filterOptions", filterOptions, "object", "null", "undefined");

            return ConnectSession.requestPromise("usergroup.list", filterOptions || {}).then((response) => {
                response.usergroups = response.usergroups.map((groupdata) => new UserGroup(groupdata));

                return response;
            });
        }

        /**
         * Retrieves userdata of a user.
         *
         * @param {string} username Username of the user to retrieve userdata
         *  for.
         * @returns {Promise<shmi.visuals.controls.iq-user-management.User>}
         */
        static getUser(username) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management");

            shmi.checkArg("username", username, "string");

            return ConnectSession.requestPromise("user.get", username).then((response) => new User(response));
        }

        /**
         * Creates a new user.
         *
         * @param {string} username Username of the new user.
         * @param {string} password Password of the new user.
         * @param {object} [additionalProperties] Additional optional user
         *  properties.
         * @param {string} [additionalProperties.firstName] First name of the
         *  person using the user.
         * @param {string} [additionalProperties.lastName] Last name of the
         *  person using the user.
         * @param {string} [additionalProperties.localeId] Locale to use.
         * @param {number} [additionalProperties.passwordValidityDuration]
         *  Number of days for wich the users password is valid. The user is
         *  prompted to renew his password afterwards.
         * @param {string[]} [additionalProperties.userGroups] Array of names
         *  of user groups associated with the user.
         * @param {boolean} [additionalProperties.disabled] Whether the user
         *  has been disabled or not. disabled users can't login.
         * @param {object} [additionalProperties.userData] Data for the users
         *  key-, value-store. Only primitive values are supported.
         * @returns {Promise<User>} Promise that is resolved with the new user.
         */
        static createUser(username, password, additionalProperties) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management"),
                newProperties = User.translateInternal2Api(additionalProperties || {});

            newProperties.username = username;
            newProperties.password = password;

            return ConnectSession.requestPromise("user.add", newProperties).then(() => new User(username, newProperties));
        }

        /**
         * Sets users properties on the backend.
         *
         * @param {object} properties
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        static setUser(username, properties) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management"),
                newProperties = User.translateInternal2Api(properties);

            newProperties.username = username;

            return ConnectSession.requestPromise("user.set", newProperties);
        }

        /**
         * Deletes a user.
         *
         * @param {string} username Username of the user to delete.
         * @returns {Promise<void>}
         */
        static deleteUser(username) {
            const { ConnectSession } = shmi.visuals.session;

            shmi.checkArg("username", username, "string");

            return ConnectSession.requestPromise("user.del", username);
        }
    };
}());
/**
 * @typedef {object} shmi.visuals.controls.iq-user-management.UserListResult
 * @property {shmi.visuals.controls.iq-user-management.User[]} users Fetched users.
 * @property {number} total_count Total number of results, ignoring offset and
 *  limit.
 */

/**
 * @typedef {object} shmi.visuals.controls.iq-user-management.UserGroupListResult
 * @property {shmi.visuals.controls.iq-user-management.UserGroup[]} groups Fetched user groups.
 * @property {number} total_count Total number of results, ignoring offset and
 *  limit.
 */

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[UserGroup]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[UserGroup]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[UserGroup]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[UserGroup]")
    };

    /**
     * A user group
     */
    module.UserGroup = class UserGroup {
        /**
         * Constructs a new user group object.
         *
         * @param {object} groupdata
         */
        constructor(groupdata) {
            const copied = {},
                { iterateObject: iterObj } = shmi.visuals.tools.iterate;

            // Create a shallow copy of the object so that changes made to
            // `properties` after this call do not change the userdata.
            iterObj(groupdata || {}, (value, key) => copied[key] = value);
            Object.defineProperty(this, "_groupdata", { value: copied, writable: false, enumerable: false, configurable: true });
        }

        /**
         * Sets the user groups properties on the backend.
         *
         * @param {object} properties
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        setProperties(properties) {
            const { ConnectSession } = shmi.visuals.session,
                { iterateObject: iterObj } = shmi.visuals.tools.iterate,
                newProperties = UserGroup.translateInternal2Api(properties);

            newProperties.groupname = this.groupname;

            return ConnectSession.requestPromise("usergroup.set", newProperties).
                then(() => iterObj(newProperties, (value, key) => this._groupdata[key] = value));
        }

        /**
         * Deletes the user group on the backend.
         *
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        delete() {
            const { ConnectSession } = shmi.visuals.session;

            return ConnectSession.requestPromise("usergroup.del", this.groupname);
        }

        /**
         * Looks up the given key in user group data and group data and returns
         * its value. Lookup location may be specified. User data takes priority
         * over group data.
         *
         * @param {string} key Key of the value to lookup.
         * @returns {*} The requested value or `null` if no value for the given
         *  key could be found.
         */
        getUserData(key) {
            shmi.checkArg("key", key, "string");

            if (shmi.objectHasOwnProperty(this._groupdata.data, key)) {
                return this._groupdata.data[key];
            }

            return null;
        }

        /**
         * Translates a Visuals user object to a user object understood by the
         * Api.
         *
         * @param {object} properties
         * @returns {object}
         */
        static translateInternal2Api(properties) {
            return {
                groupname: properties.groupname,
                data: properties.groupData,
                comment: properties.description
            };
        }

        /**
         * Name of the user group.
         * @returns {string}
         */
        get groupname() {
            return this._groupdata.groupname;
        }

        /**
         * Description.
         * @returns {?string}
         */
        get description() {
            return this._groupdata.comment;
        }
    };
}());

/**
 * Complex-Table2 configuration factory for the `user-list` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-list",
        /** @lends module:visuals.controls.iq-user-list */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Creates the `field-datagrid-col-map` property of the ct2 configuration
     * object.
     *
     * @param {DataGridUser} grid Datagrid used by complex-table2.
     * @param {string[]} columnList List of columns to display.
     */
    function makeDgColumnMap(grid, columnList) {
        const out = {};

        columnList.forEach((columnName) => {
            const idx = grid.getFields().findIndex((col) => col === columnName);
            if (idx > -1) {
                out[columnName] = idx;
            }
        });

        return out;
    }

    /**
     * Creates the `default-field-control-map` property of the ct2
     * configuration object.
     *
     * @param {string[]} columnList List of columns to display.
     */
    function makeFieldControlMap(columnList) {
        const out = {};

        columnList.forEach((columnName) => {
            out[columnName] = {
                "ui-type": "iq-label",
                "config": {
                    "options": [
                        {
                            label: "-",
                            value: -1
                        }
                    ]
                }
            };
        });

        return out;
    }

    /**
     * Creates the `column-org` property for the ct2 configuration object.
     *
     * @param {string[]} columnList List of columns to display.
     */
    function makeColumnOrg(columnList) {
        const out = {};

        columnList.forEach((columnName, idx) => {
            out[`col${idx + 1}`] = {
                fields: [columnName]
            };
        });

        return out;
    }

    /**
     * Creates a configuration object for the complex-table2 widget used by the
     * `user-list` widget.
     *
     * @param {DataGridUser} grid Datagrid used by complex-table2.
     * @param {string[]} columnList List of grid columns to display.
     */
    module.makeUserListConfig = function makeUserListConfig(grid, columnList) {
        const searchableFields = ["username", "firstName", "lastName", "localeId"].filter((columnName) => columnList.includes(columnName));
        const sortableFields = ["username", "firstName", "lastName", "localeId", "passwordValidityDuration", "passwordTimestamp", "autoLogoutDelay", "disabled"].filter((columnName) => columnList.includes(columnName));

        return {
            "table": grid.name,
            "class-name": "complex-table2",
            "field-datagrid-col-map": makeDgColumnMap(grid, columnList),
            "default-field-control-map": makeFieldControlMap(columnList),
            "default-field-headers": {
                "username": "${user_name}",
                "firstName": "${iq-user-controller.label.firstName}",
                "lastName": "${iq-user-controller.label.lastName}",
                "localeId": "${iq-user-controller.label.localeId}",
                "passwordValidityDuration": "${iq-user-controller.label.passwordExpiry}",
                "autoLogoutDelay": "${iq-user-controller.label.autoLogoutDelay}",
                "userGroups": "${iq-user-controller.label.userGroups}",
                "description": "${iq-user-controller.label.description}",
                "fakePassword": "${iq-user-controller.label.password}"
            },
            "default-layout": {
                "class-name": "layout-std",
                "column-org": makeColumnOrg(columnList),
                "line-height": "var(--table-row-height)"
            },
            "sortable-fields": sortableFields,
            "default-text-filter-fields": searchableFields,
            "quicksearch": {
                "enable": true,
                "fields": searchableFields,
                "remember": false,
                "initialFocus": false
            },
            "select-mode": "SINGLE",
            "delete-selected-rows": false,
            "show-nof-rows": true,
            "text-mode": "SINGLELINE",
            "v-scroll-options": ["SCROLLBAR", "V_SWIPE"],
            "default-nof-buffered-rows": 60,
            "buffer-size": 500
        };
    };
}());

/**
 * userlist widget
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-list",
 *     "template": "default/iq-user-list.iq-variant-01",
 *     "label": uiType,
 *     "show-last-name": true,
 *     "show-first-name": true,
 *     "show-user-groups": true,
 *     "show-password-validity": true,
 *     "show-locale": true,
 *     "show-description": true
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqUserList", // control name in camel-case
        uiType = "iq-user-list", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": `${uiType} iq-variant-01`,
        "name": null,
        "template": "default/iq-user-list.iq-variant-01",
        "label": uiType,
        "column-order": ["username", "lastName", "firstName", "userGroups", "passwordValidityDuration", "autoLogoutDelay", "localeId", "description", "fakePassword"],
        "hide-admin": false,
        "columnConfig": {
            "col1": {
                "fields": ["username"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col2": {
                "fields": ["lastName"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col3": {
                "fields": ["firstName"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col4": {
                "fields": ["userGroups"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col5": {
                "fields": ["passwordValidityDuration"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col6": {
                "fields": ["autoLogoutDelay"],
                "hidden": true,
                "minMediaQuery": false
            },
            "col7": {
                "fields": ["localeId"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col8": {
                "fields": ["description"],
                "hidden": false,
                "minMediaQuery": false
            },
            "col9": {
                "fields": ["fakePassword"],
                "hidden": true,
                "minMediaQuery": false
            }
        }
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    // declare private functions - START

    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function makeColumnSelection(config) {
        const selected = Object.values(config.columnConfig).map((columnConfig) => columnConfig.fields[0]);

        return (config["column-order"] || defConfig["column-order"]).filter((column) => selected.includes(column));
    }

    /**
     * find single column config assosiated with specified field
     *
     * @param {object} userColumnConfig user supplied column config
     * @param {string} columnName column name
     * @return {object} single column config
     */
    function findColumnConfig(userColumnConfig, columnName) {
        const config = Object.entries(userColumnConfig).find(([key, value]) => value?.fields?.[0] === columnName) ?? null;

        return config ? config[1] : null;
    }

    /**
     * generate table layout column configuration
     *
     * @param {string[]} selectedColumns selected columns
     * @param {object} userColumnConfig user column configuration
     * @return {object} table column layout configuration
     */
    function generateColumnConfig(selectedColumns, userColumnConfig) {
        const columnConfig = {};
        let columnNumber = 1;

        selectedColumns.forEach((columnName) => {
            const config = findColumnConfig(userColumnConfig, columnName);
            if (config) {
                columnConfig[`col${columnNumber}`] = config;
                columnNumber += 1;
            }
        });

        return columnConfig;
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dataGridId: null,
            dataGrid: null,
            currentUser: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const { makeUserListConfig } = shmi.requires("visuals.controls.iq-user-list"),
                    { DataGridUser } = shmi.requires("visuals.controls.iq-user-management"),
                    tableAnchor = shmi.getUiElement("table-anchor", this.element);

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                const datagrid = new DataGridUser(this.vars.dataGridId, { hideAdmin: !!this.config["hide-admin"] });
                this.imports.dgm.grids[this.vars.dataGridId] = datagrid;
                this.vars.dataGrid = datagrid;

                const selectedColumns = makeColumnSelection(this.config),
                    tableConfig = makeUserListConfig(datagrid, selectedColumns);

                tableConfig["default-layout"]["column-org"] = generateColumnConfig(selectedColumns, this.config.columnConfig ?? defConfig.columnConfig);

                if (tableAnchor) {
                    // Workaround for shmi.getControlByElement called inside
                    // shmi.createControl throwing an exception if `data-ui`
                    // is set to something that's not the type of a widget.
                    tableAnchor.removeAttribute("data-ui");
                    this.vars.ct = shmi.createControl("complex-table2", tableAnchor, tableConfig, null, "from");
                } else {
                    logger.warn("No table anchor found, inserting table directly into base-element");
                    this.vars.ct = shmi.createControl("complex-table2", this.element, tableConfig);
                }

                if (this.vars.ct === null) {
                    this.element.textContent = "Unable to create complex-table widget for users. This control will not work.";
                    logger.error("Unable to create complex-table widget for users.");
                }
            },
            /* called when control is enabled */
            onEnable: function onEnable() {
                if (!this.vars.ct) {
                    return;
                }

                this.vars.ct.enable();
                this.vars.listeners.push(this.vars.ct.listen("select", (event) => {
                    if ((event.detail.type === 1 || event.detail.type === 2) && event.detail.selRowIndex.length === 1) {
                        this.vars.currentUser = event.detail.selRowIndex[0][0];
                    } else {
                        this.vars.currentUser = null;
                    }

                    this.fire("change", { value: this.vars.currentUser }, this, true);
                }));
            },
            onLock: function onLock() {
                if (this.vars.ct) {
                    this.vars.ct.lock();
                }
            },
            onUnlock: function onUnlock() {
                if (this.vars.ct) {
                    this.vars.ct.unlock();
                }
            },
            /* called when control is disabled */
            onDisable: function onDisable() {
                this.vars.listeners.forEach((sub) => sub.unlisten());
                this.vars.listeners = [];
                if (this.vars.ct) {
                    this.vars.ct.disable();
                }
            },
            onDelete: function onDelete() {
                if (this.vars.dataGridId) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
            },
            /**
             * @returns {?string} Username of the currently selected user or
             *  `null` if no user is selected.
             */
            getCurrentUser: function getCurrentUser() {
                return this.vars.currentUser;
            },
            refresh: function refresh() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals user-select control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-select",
 *     "name": null,
 *     "template": "default/iq-user-select.iq-variant-01",
 *     "show-results": "always"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqUserSelect", // control name in camel-case
        uiType = "iq-user-select", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": `${uiType} iq-variant-01`,
        "name": null,
        "template": "default/iq-user-select.iq-variant-01",
        "label": uiType,
        "show-icon": true,
        "inline-svg-icon": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "show-results": "always",
        "hide-admin": false
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    // declare private functions - START
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function eventProxy(self, event) {
        const matchData = self.vars.searchbar.getMatchData();
        if (matchData !== null && matchData.username === event.detail.value) {
            self.fire(event.type, event.detail);
        } else {
            self.fire(event.type, { detail: null });
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domChild: null,
            dataGridId: null,
            dataGrid: null,
            searchbar: null,
            eventProxy: null
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const { DataGridUser } = shmi.requires("visuals.controls.iq-user-management");

                this.vars.domChild = shmi.getUiElement("user-select-searchbar", this.element);
                if (this.vars.domChild === null) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This widget will not work. Please select a compatible template.";
                    logger.error("Some DOM elements are missing from the template.");
                    return;
                }

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                this.imports.dgm.grids[this.vars.dataGridId] = this.vars.dataGrid = new DataGridUser(this.vars.dataGridId, { hideAdmin: !!this.config["hide-admin"] });
                this.vars.dataGrid.init();

                const searchbarConfig = {
                    "template": this.config["template"].replace(uiType, "iq-searchbar"),
                    "class-name": this.config["class-name"].split(" ").map((str) => (str === uiType ? "iq-searchbar" : str)).join(" "),
                    "label": this.config["label"],
                    "show-icon": this.config["show-icon"],
                    "icon-src": this.config["icon-src"],
                    "icon-class": this.config["icon-class"],
                    "inline-svg-icon": this.config["inline-svg-icon"],
                    "datagrid": this.vars.dataGridId,
                    "search-column": "username",
                    "show-results": this.config["show-results"],
                    "result-columns": ["username", "userGroups"]
                };

                this.vars.searchbar = shmi.createControl("iq-searchbar", this.vars.domChild, searchbarConfig);
                if (this.vars.searchbar === null) {
                    this.element.textContent = "Unable to create searchbar widget for users. This widget will not work.";
                    logger.error("Unable to create searchbar widget for users.");
                }
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {object} selected recipe info
             */
            getValue: function getValue() {
                if (this.vars.searchbar) {
                    const matchData = this.vars.searchbar.getMatchData();
                    if (matchData !== null) {
                        return this.vars.searchbar.getValue();
                    }
                }

                return null;
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function setValue(val) {
                if (this.vars.searchbar) {
                    this.vars.searchbar.setValue(val);
                }
            },
            refreshGrid: function refreshGrid() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            },
            /* called when control is enabled */
            onEnable: function onEnable() {
                if (this.vars.searchbar) {
                    this.vars.eventProxy = this.vars.searchbar.listen("change", eventProxy.bind(null, this));
                    this.vars.searchbar.enable();
                }
            },
            /* called when control is disabled */
            onDisable: function onDisable() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.disable();
                }

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }
            },
            onDelete: function onDelete() {
                if (this.vars.searchbar) {
                    shmi.deleteControl(this.vars.searchbar);
                }

                if (this.vars.dataGrid) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function onLock() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.lock();
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function onUnlock() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.unlock();
                }
            },
            /**
             * @returns {?string} Username of the currently selected user or
             *  `null` if no user is selected.
             */
            getCurrentUser: function getCurrentUser() {
                return this.getValue();
            },
            refresh: function refresh() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new ListBox control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.ListBox = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'list-box');
    shmi.def(this.config, 'template', 'default/list-box');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'label', 'list-box');
    shmi.def(this.config, 'options', []);

    this.active = false;
    this.entries = [];
    this.entryElements = [];
    this.mouseListener = null;
    this.touchListener = null;
    this.selected = -1;
    this.onSelect = null;
    this.listElement = null;

    this.startup();
};

shmi.visuals.controls.ListBox.prototype = {
    uiType: "list-box",
    events: ["select"],
    getClassName: function() {
        return "ListBox";
    },
    onInit: function() {
        var self = this;
        shmi.log("[ListBox] initializing...", 1);
        var label_el = shmi.getUiElement('label', this.element);
        if (label_el) {
            label_el.textContent = shmi.localize(this.config.label);
        }
        if (Array.isArray(self.config.options)) {
            self.config.options.forEach(function(opt) {
                self.entries.push(opt);
            });
        }
        this.listElement = shmi.getUiElement('list-items', this.element);
        if (!this.listElement) {
            shmi.notify("[ListBox] template missing 'list-items' element");
        }
        var funcs = {};
        funcs.onClick = function(x, y, event) {
            this.setSelected(event.target);
            self.fire("select", { value: self.getSelectedEntry() });
        }.bind(this);
        this.mouseListener = new shmi.visuals.io.MouseListener(this.listElement, funcs);
        this.touchListener = new shmi.visuals.io.TouchListener(this.listElement, funcs);
        shmi.log("[ListBox] initialized", 1);
    },
    /**
     * Enables the Control
     *
     */
    onEnable: function() {
        this.mouseListener.enable();
        this.touchListener.enable();
        shmi.log("[ListBox] enabled", 1);
    },
    /**
     * Disables the Control
     *
     */
    onDisable: function() {
        this.mouseListener.disable();
        this.touchListener.disable();
        shmi.log("[ListBox] disabled", 1);
    },
    addEntry: function(value) {
        if (!this.initialized) {
            setTimeout(function() {
                this.addEntry(value);
            }.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        }
        var entry = document.createElement('div');
        entry.setAttribute('data-ui', 'list-item');
        entry.setAttribute('class', 'list-item');
        entry.textContent = value;
        this.entries.push(value);
        this.entryElements.push(entry);
        this.listElement.appendChild(entry);
        shmi.log("[ListBox] entry added: " + value, 1);
    },
    removeEntryByValue: function(value) {
        var idx = -1;
        this.entries.forEach(function(ent, jdx) {
            if (ent.value === value) {
                idx = jdx;
            }
        });
        if (idx !== -1) {
            this.listElement.removeChild(this.entryElements[idx]);
            this.entries.splice(idx, 1);
            this.entryElements.splice(idx, 1);
            if (this.selected === idx) {
                this.setSelectedIndex(-1);
            }
        }
    },
    removeEntry: function(index) {
        if (index === this.selected) {
            this.selected = -1;
        }
        this.listElement.removeChild(this.entryElements[index]);
        this.entryElements.splice(index, 1);
        this.entries.splice(index, 1);
    },
    removeAll: function() {
        while (this.entries.length) {
            this.removeEntry(0);
        }
        this.selected = -1;
    },
    setSelected: function(element) {
        for (var i = 0; i < this.entryElements.length; i++) {
            if (this.entryElements[i] === element) {
                if (this.selected !== -1) {
                    shmi.removeClass(this.entryElements[this.selected], 'selected');
                }
                if (i === this.selected) {
                    this.selected = -1;
                } else {
                    this.selected = i;
                    shmi.addClass(this.entryElements[i], 'selected');
                }
                break;
            }
        }
        if (this.onSelect) {
            this.onSelect(this.selected);
        }
    },
    setSelectedIndex: function(index) {
        if (this.selected !== -1) {
            shmi.removeClass(this.entryElements[this.selected], 'selected');
        }
        if (index === this.selected) {
            this.selected = -1;
        } else if (index === -1) {
            this.selected = -1;
        } else {
            this.selected = index;
            shmi.addClass(this.entryElements[index], 'selected');
        }
        if (this.onSelect) {
            this.onSelect(this.selected);
        }
    },
    getSelectedEntry: function() {
        if (this.selected === -1) {
            return null;
        } else {
            return this.entries[this.selected];
        }
    },
    getSelectedIndex: function() {
        return this.selected;
    },
    onLock: function() {
        if (this.active) {
            this.mouseListener.disable();
            this.touchListener.disable();
            shmi.addClass(this.element, 'locked');
        }
    },
    onUnlock: function() {
        if (this.active) {
            this.mouseListener.enable();
            this.touchListener.enable();
            shmi.removeClass(this.element, 'locked');
        }
    },
    setLabel: function(labelText) {
        var self = this,
            labelElement = shmi.getUiElement("label", self.element);
        if (labelElement) {
            labelElement.textContent = shmi.localize(labelText);
        }
    }
};

shmi.extend(shmi.visuals.controls.ListBox, shmi.visuals.core.BaseControl);

/**
 * Creates a new LocalScript control.
 *
 * The LocalScript control can be used to execute arbitrary javascript code at
 * predefined places in an hmi-app. The configured script will be run every time
 * the LocalScript control is enabled.
 *
 * @constructor
 * @param {HTMLElement} element root element of control
 * @param {object} config control configuration object
 */
shmi.visuals.controls.LocalScript = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'url', null);
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'class-name', "local-script");

    this.run = false;
    this.loaded = false;

    this.script = "";

    this.startup();
};

shmi.visuals.controls.LocalScript.prototype = {
    uiType: "local-script",
    isContainer: false,
    getClassName: function() {
        return "LocalScript";
    },
    /**
     * loads configured script file
     *
     */
    onInit: function() {
        if (this.config.url) {
            shmi.loadResource(this.config.url, this._loadCB.bind(this));
        } else if (this.config.module) {
            try {
                this.module = shmi.requires(this.config.module);
                if (typeof this.module.run !== "function") {
                    throw new Error("local-script module has no 'run' function: " + this.config.module);
                }
            } catch (exc) {
                this.element.textContent = "UNKNOWN_MODULE: " + this.config.module;
                this.module = {
                    run: function() {}
                };
            }
            this.loaded = true;
        } else {
            console.log("[LocalScript] no script configured");
        }
    },
    /**
     * callback for load of script file. executes the script if the control is
     * enabled when the callback is run.
     *
     * @param {string} data script data
     * @param {boolean} failed true if script load failed, false else
     */
    _loadCB: function(data, failed) {
        if (!failed) {
            this.script = data;
            this.loaded = true;
            if (this.active) {
                this.run = true;
                eval(this.script);
            }
        } else {
            console.log("[LocalScript] failed to load script:", this.config.url);
        }
    },
    /**
     * run when control is enabled. executes configured script.
     *
     */
    onEnable: function() {
        if (!this.run && this.loaded) {
            if (this.config.module) {
                this.module.run(this);
            } else {
                eval(this.script);
            }
        }
    },
    /**
     * run when control is disabled
     *
     */
    onDisable: function() {
        this.run = false;
    }
};

shmi.extend(shmi.visuals.controls.LocalScript, shmi.visuals.core.BaseControl);

(function() {
    shmi.pkg("visuals.controls");

    const LOGIN_ERROR_MAP = {
        "auth-generic": {
            3: "${login.error.invalid-credentials}",
            10: "${login.error.not-supported}",
            11: "${login.error.not-implemented}",
            14: "${login.error.realm-not-connected}",
            15: "${login.error.authentication-not-available}",
            16: "${login.error.internal-error}"
        },
        "shmi:connect:api:user": {
            3: "${login.error.invalid-credentials}"
        },
        "shmi:connect:license": {
            4: "${login.error.too-many-clients}"
        }
    };

    function getLoginErrorReason(errorObject) {
        if (!errorObject || typeof errorObject !== "object") {
            return null;
        }

        return LOGIN_ERROR_MAP[errorObject.category]?.[errorObject.errc] ?? null;
    }

    /**
     * Retrieves user realm of a user.
     * @returns {Promise<shmi.visuals.controls.iq-user-management.User>}
    */
    function getUserRealms() {
        const { ConnectSession } = shmi.visuals.session;

        return ConnectSession.requestPromise("user.realms", {});
    }

    /**
     * Creates a Auth-Realm Select-Box
     *
     * @param {object} self
     * @param {object} options select-box options
    */
    function createAuthSelectBox(self, options) {
        const session = shmi.requires("visuals.session"),
            selectBoxConfig = Object.assign({}, self.config.authBox, { options });

        if (selectBoxConfig.options.length >= 2) {
            selectBoxConfig['class-name'] = selectBoxConfig['class-name'].replace(' hidden', '');
        }

        const selectBox = shmi.createControl("iq-select-box", self.authRealmElement, selectBoxConfig, "DIV", "from");

        shmi.waitOnInit(selectBox, function() {
            if (localStorage.getItem('webiq-auth-realm')) {
                selectBox.setValue(localStorage.getItem('webiq-auth-realm'));
            }

            self.token.push(selectBox.listen('change', function onChange(event) {
                const realm = event.detail.value;
                localStorage.setItem('webiq-auth-realm', realm);
                session.UserManager.requestUserList(self.applyUserList.bind(self), realm);
            }));
        });

        self.authRealmSelectBox = selectBox;
    }

    /**
     * Creates a new Login control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {HTMLElement} element root element of the control
     * @param {Object} config control configuration
     */
    shmi.visuals.controls.Login = function(element, config) {
        this.element = element;

        this.config = config || {};

        this.parseAttributes();

        shmi.def(this.config, 'class-name', 'login');
        shmi.def(this.config, 'template', 'default/login');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'userEntry', '<div data-ui="user" class="user"><div class="userPic"></div><div class="user_detail"><h1 data-ui="user-name"><%= USER %></h1></div></div>');

        shmi.def(this.config, 'authBox', {});
        shmi.def(this.config.authBox, 'class-name', 'iq-variant-01 no-icon hidden');
        shmi.def(this.config.authBox, 'label', 'Authentication Realm');
        shmi.def(this.config.authBox, 'template', 'default/iq-select-box.iq-variant-01');
        shmi.def(this.config.authBox, 'selected', 0);

        this.userElement = null;
        this.passwordElement = null;
        this.loginButtonElement = null;
        this.clearButtonElement = null;
        this.autoLoginButtonElement = null;
        this.clickCounter = 0;

        this.authRealmElement = null;
        this.authRealmSelectBox = null;
        this.authRealm = null;

        this.controls = [];
        this.token = [];

        if (/Firefox/.test(navigator.userAgent)) {
            this.innerWidth = window.innerWidth - 1;
            this.innerHeight = window.innerHeight - 1;
        } else {
            this.innerWidth = window.innerWidth;
            this.innerHeight = window.innerHeight;
        }

        this.userListContent = "";

        this.startup();
    };

    shmi.visuals.controls.Login.prototype = {
        uiType: "login",
        isContainer: true,
        getClassName: function() {
            return "Login";
        },
        onInit: function() {
            const self = this,
                session = shmi.requires("visuals.session");

            if (!self.element) {
                shmi.log("[Login] no base element provided", 3);
                return;
            }
            self.userElement = shmi.getUiElement('user-field', self.element);
            if (!self.userElement) {
                shmi.log("[Login] no user-field element provided", 3);
                return;
            }
            shmi.addClass(self.userElement, 'locked');
            self.userElement.setAttribute('disabled', true);
            self.passwordElement = shmi.getUiElement('password-field', self.element);
            if (!self.passwordElement) {
                shmi.log("[Login] no password-field element provided", 3);
                return;
            }
            shmi.addClass(self.passwordElement, 'locked');
            self.passwordElement.setAttribute('disabled', true);
            self.loginButtonElement = shmi.getUiElement('login-button', self.element);
            if (!self.loginButtonElement) {
                shmi.log("[Login] no login-button element provided", 3);
                return;
            }
            self.clearButtonElement = shmi.getUiElement('clear-button', self.element);
            if (!self.clearButtonElement) {
                shmi.log("[Login] no clear-button element provided", 3);
                return;
            }

            self.autoLoginButtonElement = shmi.getUiElement('autologin-button', self.element);
            if (!self.autoLoginButtonElement) {
                shmi.log("[Login] no autologin-button element provided", 2);
            }

            /* initialize connection status display */
            self.statusElement = shmi.getUiElement('status', self.element);
            if (self.statusElement) {
                self.statusLabelElement = shmi.getUiElement('label', self.statusElement);
            }

            if (self.statusElement && self.statusLabelElement) {
                shmi.addClass(self.statusElement, 'connecting');
                self.statusLabelElement.textContent = 'connecting ...';
            }

            self.authRealmElement = shmi.getUiElement('iq-select-box', self.element);
            if (!self.authRealmElement) {
                shmi.log("[Login] no realms-select-box element provided", 3);
            }

            /* establish connection to configured connect-host */
            session.SocketConnection.connect(function(status) {
                const storageRealm = localStorage.getItem('webiq-auth-realm');

                if (status === 0) {
                    session.UserManager.requestUserList(self.applyUserList.bind(self), storageRealm);

                    getUserRealms().
                        then(function(result) {
                            const authOptions = [];

                            result = result.filter((realm) => realm.realm_name !== "builtin");
                            authOptions.push(...result.map((realm) => ({
                                label: realm.display_name,
                                value: realm.realm_name
                            })));

                            if (authOptions.length && self.authRealmElement) {
                                // Authentication Realm Select-box
                                createAuthSelectBox(self, authOptions);
                            }
                        }).catch((e) => {
                            console.error('No realms were found', e);
                        });
                } else if (self.statusElement && self.statusLabelElement) {
                    shmi.removeClass(self.statusElement, 'connecting');
                    shmi.addClass(self.statusElement, 'error');
                    self.statusLabelElement.textContent = 'connection error';
                } else {
                    console.error("[Login] Error establishing WebSocket connection");
                }
            });

            const request = new XMLHttpRequest();
            request.open("GET", '.webiq/preview.png', true);
            request.send();
            request.onload = function() {
                if (request.status !== 200) {
                    document.getElementById('project-image').style.backgroundImage = 'url("pics/system/controls/login/placeholder.png")';
                }
            };

            /* all elements found, set reference to login in session */
            session.Login = this;
        },
        onEnable: function() {
            if (Array.isArray(this.listeners)) {
                this.listeners.forEach(function(lst) {
                    lst.enable();
                });
            }
        },
        onDisable: function() {
            if (Array.isArray(this.listeners)) {
                this.listeners.forEach(function(lst) {
                    lst.disable();
                });
            }

            if (Array.isArray(this.token)) {
                this.token.forEach(function(lst) {
                    lst.unlisten();
                });
            }
        },
        /**
         * Applies retrieved user list
         *
         */
        applyUserList: function() {
            const self = this,
                users = shmi.visuals.session.UserManager.userList,
                iter = shmi.requires("visuals.tools.iterate.iterateObject");

            /* clear user list */
            self.userListContent = "";
            /* rebuild user list */
            iter(users, function(val, name) {
                self.userListContent += shmi.evalString(self.config.userEntry, { USER: name });
            });

            this.userListElement = document.createElement('div');
            shmi.addClass(this.userListElement, 'hidden');
            shmi.addClass(this.userListElement, 'iqel-user-list');

            /* create user-list tool-tip */
            const taDiv = document.createElement('div');
            taDiv.className = 'iqel-toolArrow';
            const ttDiv = document.createElement('div');
            ttDiv.className = 'iqel-toolTip';
            ttDiv.innerHTML = this.userListContent;
            this.userListElement.appendChild(taDiv);
            this.userListElement.appendChild(ttDiv);

            this.userNameInputElement = shmi.getUiElement('user-name-input', this.element);
            if (this.userNameInputElement) {
                this.userNameInputElement.appendChild(this.userListElement);
            }

            const um = shmi.visuals.session.UserManager,
                ses = shmi.visuals.session;

            let appUrl = null;

            um.requestAutoLogin((error) => {
                if (this.statusElement && this.statusLabelElement) {
                    this.statusLabelElement.textContent = 'connected';
                    shmi.removeClass(this.statusElement, 'connecting');
                    shmi.addClass(this.statusElement, 'connected');
                }

                if (error) {
                    shmi.removeClass(this.userElement, 'locked');
                    shmi.removeClass(this.passwordElement, 'locked');
                    this.userElement.removeAttribute('disabled');
                    this.passwordElement.removeAttribute('disabled');
                    this.initListeners();
                    this.loginCallback(error);
                } else {
                    this.loginCallback(0);
                }
            });

            if (ses.URLParameters.autoLogin) {
                appUrl = new URL(window.location.href);
                appUrl.searchParams.delete("autoLogin");
                window.history.replaceState({}, document.title, appUrl.toString());
            }
        },
        initListeners: function() {
            const self = this;

            /* add event listeners */
            this.userElement.addEventListener('focus', function() {
                shmi.removeClass(this.userListElement, 'hidden');
            }.bind(this));

            this.userElement.addEventListener('click', function() {
                const appConfig = shmi.visuals.session.config,
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                if (!keyboardEnabled) { //does nothing if not enabled
                    return;
                }

                this.clickCounter++;
                if (this.clickCounter > 1) {
                    const params = {
                        "value": self.userElement.value,
                        "password-input": false,
                        "select-box-enabled": true,
                        "callback": function(success, input) {
                            if (success) {
                                self.userElement.value = input;
                            }
                        }
                    };

                    shmi.keyboard(params);
                }
            }.bind(this));

            this.userElement.addEventListener('blur', function() {
                if (this.clickOnUserList) {
                    return;
                }
                shmi.addClass(this.userListElement, 'hidden');
            }.bind(this));

            this.passwordElement.addEventListener('focus', function() {
                const appConfig = shmi.visuals.session.config,
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`
                if (!keyboardEnabled) { //does nothing if not enabled
                    return;
                }

                const params = {
                    "value": self.passwordElement.value,
                    "password-input": true,
                    "select-box-enabled": true,
                    "callback": function(success, input) {
                        if (success) {
                            self.passwordElement.value = input;
                        }
                    }
                };

                shmi.keyboard(params);
            });

            const userfields = shmi.getUiElements('user', this.userListElement);
            if (Array.isArray(this.listeners)) {
                this.listeners.forEach(function(lst) {
                    lst.disable();
                });
            }
            this.listeners = [];
            for (let i = 0; i < userfields.length; i++) {
                const funcs = {};

                funcs.onPress = function() {
                    this.clickOnUserList = true;
                }.bind(this);
                funcs.onRelease = function() {
                    this.clickOnUserList = false;
                }.bind(this);
                funcs.onClick = (function(field) {
                    this.listeners.push(new shmi.visuals.io.MouseListener(field, funcs));
                    this.listeners.push(new shmi.visuals.io.TouchListener(field, funcs));
                    return function() {
                        const username = shmi.getUiElement('user-name', field);

                        this.userElement.value = username.innerHTML;
                        shmi.log("[Login] username: " + username.innerHTML, 1);
                        shmi.decouple(function() {
                            shmi.addClass(this.userListElement, 'hidden');
                            this.passwordElement.focus();
                        }.bind(this));
                    }.bind(this);
                }.bind(this))(userfields[i]);
            }

            this.userElement.addEventListener('keypress', function(event) {
                if (event.keyCode === 13) {
                    shmi.decouple(function() {
                        this.passwordElement.focus();
                    }.bind(this));
                }
            }.bind(this));

            this.passwordElement.addEventListener('keypress', function(event) {
                if (event.keyCode === 13) {
                    this.passwordElement.blur();
                    shmi.decouple(function() {
                        if (self.authRealmElement) self.authRealm = self.authRealmSelectBox.getValue();
                        shmi.visuals.session.UserManager.requestLogin(this.userElement.value.trim(), this.passwordElement.value, this.loginCallback.bind(this), self.authRealm);
                    }.bind(this));
                }
            }.bind(this));

            const logBtnFuncs = {};
            logBtnFuncs.onClick = function() {
                if (self.authRealmElement) self.authRealm = self.authRealmSelectBox.getValue();
                shmi.visuals.session.UserManager.requestLogin(this.userElement.value.trim(), this.passwordElement.value, this.loginCallback.bind(this), self.authRealm);
            }.bind(this);
            this.listeners.push(new shmi.visuals.io.MouseListener(this.loginButtonElement, logBtnFuncs));
            this.listeners.push(new shmi.visuals.io.TouchListener(this.loginButtonElement, logBtnFuncs));

            const clrBtnFuncs = {};
            if (this.clearButtonElement) {
                clrBtnFuncs.onClick = function() {
                    this.userElement.value = '';
                    this.passwordElement.value = '';
                }.bind(this);
                this.listeners.push(new shmi.visuals.io.MouseListener(this.clearButtonElement, clrBtnFuncs));
                this.listeners.push(new shmi.visuals.io.TouchListener(this.clearButtonElement, clrBtnFuncs));
            }

            if (this.autoLoginButtonElement) {
                const io = shmi.requires("visuals.io"),
                    s = shmi.requires("visuals.session"),
                    um = s.UserManager,
                    observerFuncs = {
                        onClick: function() {
                            um.requestLogin(s.config['auto-login'].user, s.config['auto-login'].password, self.loginCallback.bind(self));
                        }
                    },
                    labelElement = shmi.getUiElement("button-label", this.autoLoginButtonElement);

                //perform auto login
                if (s.config['auto-login'] && s.config['auto-login'].enabled) {
                    if (labelElement) {
                        labelElement.textContent = shmi.evalString("Login as default user '<%= USER %>'", {
                            USER: s.config['auto-login'].user
                        });
                    }
                    shmi.removeClass(this.autoLoginButtonElement, "hidden");
                    shmi.removeClass(this.loginButtonElement, "filled");
                    this.listeners.push(new io.MouseListener(this.autoLoginButtonElement, observerFuncs));
                    this.listeners.push(new io.TouchListener(this.autoLoginButtonElement, observerFuncs));
                }
            }

            if (this.active === true) {
                this.listeners.forEach(function(lst) {
                    lst.enable();
                });
            }
        },
        /**
         * Called after login request returns
         *
         * @param {object} error error information if login failed
         */
        loginCallback: function(error) {
            shmi.log("[Login] login-callback called", 2);
            const s = shmi.visuals.session;
            if (s.UserManager.currentUser && s.UserManager.currentUser.loggedIn) {
                shmi.log("[Login] login succeeded", 1);
                const self = this;
                /* clear DOM */
                document.body.innerHTML = "";

                if (s.config['show-load-screen']) {
                    /* create loading overlay */
                    const overlayConfig = shmi.pkg("visuals.session.SysControlConfig.loadingOverlay");
                    this.bgDiv = document.createElement(overlayConfig.tagName || 'DIV');
                    shmi.addClass(this.bgDiv, overlayConfig['class-name'] || 'loading-box-overlay');
                    if (overlayConfig.template) {
                        shmi.loadResource(shmi.c("TEMPLATE_PATH") + overlayConfig.template + shmi.c("TEMPLATE_EXT"), function(data, failed) {
                            if (!failed) {
                                self.bgDiv.innerHTML = data;
                            }
                        });
                    }
                    document.body.appendChild(this.bgDiv);
                }
                /* retrieve alarm-list */
                s.AlarmManager.requestAlarmList();

                /* load locale */
                shmi.decouple(function() {
                    shmi.loadResource(shmi.evalString(shmi.c("LOCALE_PATH_PATTERN"), { index: s.UserManager.currentUser.locale }), self.loadProject.bind(self));
                });
            } else if (error?.message === "Auto-Login credentials exhausted") {
                // We silently ignore this one.
            } else {
                shmi.notify(getLoginErrorReason(error) ?? "${login.error.invalid-credentials}", "${V_NOTIFICATION}");

                if (error?.message) {
                    shmi.log(`[Login] login failed - ${error.message}`, 1);
                } else {
                    shmi.log("[Login] login failed", 1);
                }
            }
        },
        /**
         * Applies loaded project configuration
         *
         * @param {string} responseText - content of project configuration
         * @param {boolean} failed - true if loading failed, false else
         * @param {string} url - url of loaded locale
         */
        loadProject: function(responseText, failed, url) {
            const session = shmi.requires("visuals.session");
            session.locale = session.locale || {};
            if (!failed) {
                const iter = shmi.requires("visuals.tools.iterate.iterateObject");

                let locale = {};

                try {
                    locale = JSON.parse(responseText);
                    iter(locale, function(val, prop) {
                        session.locale[prop] = val;
                    });
                } catch (exc) {
                    console.error("[Login] failed to parse locale file:", url, exc);
                }
            }

            let layout = null;
            if (typeof session.config.layout === "string") {
                layout = session.config.layout;
            } else {
                layout = shmi.c("DEFAULT_LAYOUT_URL");
            }

            if (session.URLParameters.layout !== undefined) {
                layout = session.URLParameters.layout;
            }
            session.LayoutURL = layout;
            this.layout_url = layout;

            if (layout.endsWith(".json")) {
                shmi.loadResource(`json/layouts/${layout}`, (layoutResponse, layoutFailed) => {
                    const model = shmi.requires("visuals.model");
                    if (!layoutFailed) {
                        try {
                            model.parse(layoutResponse);
                            this.applyProject(model.getTemplate("root"), false);
                        } catch (err) {
                            console.error("Error parsing app model:", err);
                            this.applyProject(null, true);
                        }
                    } else {
                        this.applyProject(null, true);
                    }
                });
            } else {
                shmi.loadResource(layout, this.applyProject.bind(this));
            }
        },
        /**
         * Applies the loaded project entrypoint
         *
         * @param responseText - content of project entrypoint
         * @param failed - true if loading failed, false elese
         */
        applyProject: function(responseText, failed) {
            if (failed) {
                shmi.log("[Login] could not load project entrypoint.\n" + this.layout_url, 3);
                return;
            }
            const s = shmi.visuals.session;
            /* Set Project Content */
            const docFragment = document.createDocumentFragment();
            const fragDiv = document.createElement('div');
            fragDiv.innerHTML = responseText;
            docFragment.appendChild(fragDiv);
            while (docFragment.firstChild.firstChild) {
                document.body.appendChild(docFragment.firstChild.firstChild);
            }
            s.ProjectSource = responseText;

            /* parse project */
            let controlInstances = shmi.visuals.parser.parseProject();
            const viewportMeta = document.getElementById('viewportMeta');
            if (viewportMeta) {
                viewportMeta.setAttribute('content', viewportMeta.getAttribute('content').replace(
                    'user-scalable=no', 'user-scalable=yes'));
            }

            /* check init status of controls */
            shmi.waitOnInit(controlInstances, () => {
                const iter = shmi.requires("visuals.tools.iterate.iterateObject");

                let resizeEvt = null;

                shmi.log("[Login] All controls initialized", 2);

                /* do not run startscripts if none define or shmi-project is active */
                if (shmi.visuals.session.startScripts && (shmi.visuals.session.config["session-client"] === "visuals.defaultClient")) {
                    iter(shmi.visuals.session.startScripts, function(val, script) {
                        try {
                            shmi.visuals.session.startScripts[script]();
                        } catch (exc) {
                            shmi.log("[Login] failed to execute start-script '" + script + "': " + exc, 2);
                        }
                    });
                }

                /* fire resize event to let controls know that the layout is complete */
                resizeEvt = document.createEvent("Event");
                resizeEvt.initEvent("resize", true, true);
                window.dispatchEvent(resizeEvt);

                /* fade out load screen if present */
                if (s.config['show-load-screen']) {
                    shmi.decouple(function() {
                        if (s.config['load-screen-transition'] === false) {
                            document.body.removeChild(this.bgDiv);
                        } else {
                            this.bgDiv.style.opacity = 0;
                            this.bgDiv.addEventListener('transitionend', function() {
                                document.body.removeChild(this.bgDiv);
                            }.bind(this));
                            this.bgDiv.addEventListener('webkitTransitionEnd', function() {
                                document.body.removeChild(this.bgDiv);
                            }.bind(this));
                        }
                    }.bind(this));
                }
                shmi.fire('parser-ready', {}, shmi.visuals.session);
            });
        }
    };

    shmi.extend(shmi.visuals.controls.Login, shmi.visuals.core.BaseControl);
}());

/**
 * Numpad
 *
 * Configuration options (default):
 *
 * {
 *      "class-name": uiType,
 *      "name": null,
 *      "template": "default/numpad/numpad",
 *      "label": uiType,
 *      "infoblockLimitsLabel": "${NUMPAD_LIMITS}",
 *      "inputLimitMinimumLabel": "${NUMPAD_INPUT_LIMIT_MINIMUM}",
 *      "inputLimitMaximumLabel": "${NUMPAD_INPUT_LIMIT_MAXIMUM}",
 *      "initially-visible": false,
 *      "top-level": false,
 *      "value": "0",                      // Init value to modify.
 *      "callback": null,                  // callback with the result value. Will be returned like "callback(Number value)"
 *      "unit": "",                        // what we are counting. e.g.: "%" or "$" -> "95,50 %""
 *      "precision": ",",                  // character to display as the decimal dot
 *      "max-chars": 10,                   // number of max characters of the input length
 *      "type": shmi.c("TYPE_FLOAT"),      // can be "TYPE_FLOAT" or "TYPE_INT"
 *      "precision": 2,                    // numbers after the decimal dot. Set to "-1" if unlimited.
 *      "min": -100,                       // number below this cannot be submitted to the callback
 *      "max": 100                         // number larger than this cannot be submitted to the callback
 *      "enable-key-codes": [1,2]          // Array of keycodes - unconfigurable through Designer
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    const className = "Numpad", //control name in camel-case
        uiType = "numpad", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/numpad/numpad-box",
        "label": uiType,
        "inputLimitMinimumLabel": "${NUMPAD_INPUT_LIMIT_MINIMUM}", // AUTO_OR_INPUT_LOCALIZED_STRING
        "inputLimitMaximumLabel": "${NUMPAD_INPUT_LIMIT_MAXIMUM}", // AUTO_OR_INPUT_LOCALIZED_STRING
        "initially-visible": false, // checkbox
        "top-level": false,
        "value": "0", // INPUT_NUMERIC
        "max-chars": 10, // INPUT_NUMERIC
        "callback": null, // X
        "unit": "", // UNIT_TEXT
        "decimal-delimiter": "${DECIMAL_DELIMITER}", // TEXT
        "type": shmi.c("TYPE_FLOAT"), // DROPDOWN
        "precision": 2, // NUMERIC
        "min": -100, // NUMERIC
        "max": 100, // NUMERIC
        "enable-key-codes": [] // X
    };

    //setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * errMessage object contains several function to add or remove the messages from the numpad
     *
     */
    const errMessage = {
        add: function(self, name, msgText, timeout) {
            if (self.vars.errTextElem && name && msgText) {
                if (!self.vars.messages[name]) {
                    if (Object.keys(self.vars.messages).length === 0) {
                        shmi.addClass(self.vars.errTextElem, "visible");
                    }

                    self.vars.messages[name] = document.createElement("div");
                    shmi.addClass(self.vars.messages[name], 'err-msg-' + name);
                    self.vars.errTextElem.appendChild(self.vars.messages[name]);

                    shmi.addClass(self.vars.messages[name], 'fadein');
                    setTimeout(function() {
                        if (self.vars.messages[name]) {
                            shmi.removeClass(self.vars.messages[name], 'fadein');
                        }
                    }, 250);
                }

                self.vars.messages[name].innerHTML = msgText;
                if (!isNaN(Number(timeout))) {
                    setTimeout(function() {
                        errMessage.remove(self, name);
                    }, Number(timeout));
                }
            }
        },
        remove: function(self, name) {
            if (self.vars.errTextElem && name && self.vars.messages[name]) {
                shmi.addClass(self.vars.messages[name], 'fadeout');

                setTimeout(function() {
                    if (self.vars.messages[name]) {
                        self.vars.errTextElem.removeChild(self.vars.messages[name]);
                    }
                    delete self.vars.messages[name];
                }, 250);
            }
        },
        clear: function(self) {
            if (self.vars.errTextElem && Object.keys(self.vars.messages).length > 0) {
                Object.keys(self.vars.messages).forEach(function(name) {
                    errMessage.remove(self, name);
                });
                self.vars.messages = {};
                setTimeout(function() {
                    shmi.removeClass(self.vars.errTextElem, "visible");
                    self.vars.errTextElem.innerHTML = '';
                }, 250);
            }
        }
    };

    // declare private functions - START
    /**
     * setter/getter for the result
     * @param {object} self
     * @param {string|undefined} val
     */
    function value(self, val) {
        if (val !== undefined) {
            self.config.value = val;
        }
        if (validate(self.config.value, self) || self.config.value === '') {
            shmi.removeClass(self.vars.inputDisplayElement, 'error');
        } else {
            shmi.addClass(self.vars.inputDisplayElement, 'error');
        }

        return self.config.value;
    }

    /**
     * validate value in the context self
     * @param {string} val
     * @param {object} self
     */
    function validate(val, self) {
        let valSplit,
            regExp = new RegExp('[^0-9\'' + self.config["decimal-delimiter"] + '-]', 'i'),
            valAsNumber = val === "" ? null : Number(String(val).replace(self.config["decimal-delimiter"], ".")),
            msg = '';

        // Change input maxlength attiubute according to presence of decimal-delimiter and minus
        let additionalChars = 0;
        if (val.indexOf(self.config["decimal-delimiter"]) !== -1) {
            additionalChars++;
        }
        if (val.indexOf('-') === 0) {
            additionalChars++;
        }
        self.vars.inputDisplayElement.setAttribute("maxlength", self.config["max-chars"] + additionalChars);
        if (valAsNumber === null) {
            errMessage.remove(self, 'min-error');
            errMessage.remove(self, 'max-error');
            return false;
        } else if (self.config.type === shmi.c("TYPE_INT") && !Number.isInteger(valAsNumber)) {
            msg = shmi.localize('${numpad_entered_value_is_not_integer}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.remove(self, 'no-number');
            errMessage.add(self, 'no-integer', msg);
            return false;
        } else if (isNaN(valAsNumber)) {
            msg = shmi.localize('${numpad_entered_value_is_not_number}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.add(self, 'no-number', msg);
            errMessage.remove(self, 'no-integer');
            return false;
        } else {
            errMessage.remove(self, 'no-number');
            errMessage.remove(self, 'no-integer');
        }

        if (val.match(regExp)) {
            msg = shmi.localize('${numpad_entered_value_invalid}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.add(self, 'invalid', msg);
            return false;
        } else {
            errMessage.remove(self, 'invalid');
        }

        if ((Number(self.config.precision) !== -1) && (val.indexOf(self.config["decimal-delimiter"]) !== -1)) {
            valSplit = val.split(self.config["decimal-delimiter"]);
            if (valSplit[1] !== undefined && valSplit[1].length > self.config.precision) {
                msg = shmi.evalString(shmi.localize('${numpad_too_many_digits}'), {
                    precision: self.config.precision
                });
                shmi.log("[Numpad] " + msg, 1);
                errMessage.add(self, 'exceeded-decimal-digits', msg);
                return false;
            } else {
                errMessage.remove(self, 'exceeded-decimal-digits');
            }
        } else {
            errMessage.remove(self, 'exceeded-decimal-digits');
        }

        if ((typeof self.config.min === "number") && valAsNumber < self.config.min) {
            msg = shmi.localize('${numpad_entered_value_too_low}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.remove(self, 'max-error');
            errMessage.add(self, 'min-error', msg);
            return false;
        } else {
            errMessage.remove(self, 'min-error');
        }

        if ((typeof self.config.max === "number") && valAsNumber > self.config.max) {
            msg = shmi.localize('${numpad_entered_value_too_large}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.remove(self, 'min-error');
            errMessage.add(self, 'max-error', msg);
            return false;
        } else {
            errMessage.remove(self, 'max-error');
        }

        errMessage.clear(self);
        return true;
    }

    /**
     * insert char into the string and replace selected parts if any
     * @param {string} val
     * @param {string} string
     * @param {object} self
     */
    function insertCharIntoString(val, string, self) {
        string = String(string);

        // Allow additional Chars for decimal-delimiter and minus
        let additionalChars = 0;
        if (string.indexOf(self.config["decimal-delimiter"]) !== -1) {
            additionalChars++;
        }
        if (string.indexOf('-') === 0) {
            additionalChars++;
        }
        let newStr = false,
            maxChars = Number(self.config["max-chars"] + additionalChars);

        newStr = string.substr(0, self.vars.inputDisplayElement.selectionStart) + val + string.substr(self.vars.inputDisplayElement.selectionEnd);

        // replace first 0 with new val but add comma after it
        if (string === "0" && val !== self.config["decimal-delimiter"]) {
            newStr = val;
            // Check if new string exceeds the range of maximum characters
        } else if (maxChars && newStr.length > maxChars) {
            const msg = shmi.evalString(shmi.localize('${numpad_too_many_characters}'), {
                maxChars: maxChars - additionalChars
            });
            shmi.log("[Numpad] " + msg, 3);
            errMessage.add(self, 'max-chars', msg, 3000);

            // if multiple chars are pasted, insert only allowed length of val
            if (val.length > 1) {
                val = val.substr(0, (maxChars - string.length));
                return string.substr(0, self.vars.inputDisplayElement.selectionStart) + val + string.substr(self.vars.inputDisplayElement.selectionEnd);
            } else {
                return string;
            }
        }
        self.vars.inputDisplayElement.selectionStart = self.vars.inputDisplayElement.selectionEnd;
        self.vars.inputDisplayElement.focus();
        return newStr;
    }

    /**
     * @param {string} elId
     * @param {object} self
     */
    function onNumpadAction(elId, self, el) {
        let newValue;
        if (elId === undefined || self.vars.buttonsElements[elId] === undefined || el.tagName.toLowerCase() === 'input') {
            return;
        }
        newValue = self.vars.buttons[elId].action(value(self));
        self.vars.inputDisplayElement.value = value(self, newValue);
    }

    /** @param {object} self */
    function actionOk(self) {
        if (!validate(value(self), self)) {
            return;
        }
        if (typeof self.config.callback === "function") {
            self.config.callback(Number(value(self).replace(",", ".")));
        }
        self.hide();
    }

    /** @param {object} self */
    function actionClose(self) {
        self.hide();
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            titleElement: null,
            inputLimitMinimum: null,
            inputLimitMinimumLabel: null,
            inputLimitMaximum: null,
            inputLimitMaximumLabel: null,
            inputDisplayElement: null,
            errTextElem: null,
            unitElement: null,
            buttons: {},
            buttonsElements: [],
            containerElement: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        events: ["open", "ok", "close"],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;
                self.config["decimal-delimiter"] = shmi.localize(self.config["decimal-delimiter"]);

                // object to store error self.vars.messages in
                self.vars.messages = {};
                // Stringify the numeric value.
                self.config.value = String(self.config.value).replace(".", self.config["decimal-delimiter"]);

                // define valid key codes for keyboard inputs
                self.vars.keys = {
                    number: [48, 49, 50, 51, 52, 53, 54, 55, 56, 57],
                    numpad: [96, 97, 98, 99, 100, 101, 102, 103, 104, 105],
                    arrow: [37, 38, 39, 40],
                    startEndDelete: [35, 36, 46],
                    decimalDelimiters: [110, 188, 190],
                    minus: [189, 109],
                    plus: [107, 187],
                    ctrls: [/* backspace */ 8, /* enter */ 13, /* ctrl */ 17, /* esc*/ 27],
                    functionKeys: [112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123],
                    configKeyCodes: self.config["enable-key-codes"].map(function(code) {
                        return Number(code);
                    })
                };

                // Workaround for target versions of Edge not having Array.prototype.flat().
                // Equivalent: Object.values(self.vars.keys).flat()
                self.vars.keys.validKey = Array.prototype.concat.apply([], Object.values(self.vars.keys));
                self.vars.keys.validNumber = self.vars.keys.number.concat(self.vars.keys.numpad);

                // Define button actions here
                self.vars.buttons = {
                    "1": {
                        action: function(val) {
                            return insertCharIntoString("1", val, self);
                        }
                    },
                    "2": {
                        action: function(val) {
                            return insertCharIntoString("2", val, self);
                        }
                    },
                    "3": {
                        action: function(val) {
                            return insertCharIntoString("3", val, self);
                        }
                    },
                    "4": {
                        action: function(val) {
                            return insertCharIntoString("4", val, self);
                        }
                    },
                    "5": {
                        action: function(val) {
                            return insertCharIntoString("5", val, self);
                        }
                    },
                    "6": {
                        action: function(val) {
                            return insertCharIntoString("6", val, self);
                        }
                    },
                    "7": {
                        action: function(val) {
                            return insertCharIntoString("7", val, self);
                        }
                    },
                    "8": {
                        action: function(val) {
                            return insertCharIntoString("8", val, self);
                        }
                    },
                    "9": {
                        action: function(val) {
                            return insertCharIntoString("9", val, self);
                        }
                    },
                    "0": {
                        action: function(val) {
                            // avoid multiple zeros
                            return val === "0" ? "0" : insertCharIntoString("0", val, self);
                        }
                    },
                    "dot": {
                        action: function(val) {
                            val = String(val);
                            if (val.indexOf(self.config["decimal-delimiter"]) !== -1) {
                                return (val);
                            }
                            const addedStr = val.match(/\d/) === null ? `0${self.config["decimal-delimiter"]}` : self.config["decimal-delimiter"];
                            return insertCharIntoString(addedStr, val, self);
                        }
                    },
                    "backspace": {
                        action: function(val) {
                            val = String(val);
                            if (self.vars.inputDisplayElement.selectionStart === self.vars.inputDisplayElement.selectionEnd) {
                                if (self.vars.inputDisplayElement.selectionStart === 0) {
                                    self.vars.inputDisplayElement.selectionStart = val.length;
                                    self.vars.inputDisplayElement.selectionEnd = val.length;
                                }
                                return val.substr(0, self.vars.inputDisplayElement.selectionStart - 1) + val.substr(self.vars.inputDisplayElement.selectionEnd);
                            }
                            return val.substr(0, self.vars.inputDisplayElement.selectionStart) + val.substr(self.vars.inputDisplayElement.selectionEnd);
                        }
                    },
                    "plusMinus": {
                        action: function(val) {
                            if (val.indexOf("-") !== -1) {
                                val = String(val).replace("-", "");
                            } else {
                                val = "-" + String(val);
                            }
                            return val;
                        }
                    },
                    "close": {
                        action: function(val) {
                            actionClose(self);
                            return val;
                        }
                    },
                    "ok": {
                        action: function(val) {
                            actionOk(self);
                            return val;
                        }
                    }
                };

                // if all elements are existing - init them
                self.vars.titleElement = shmi.getUiElement('numpad-title', self.element);
                if (!self.vars.titleElement) {
                    shmi.log('[Numpad] no numpad-title element provided', 3);
                }
                self.vars.errTextElem = shmi.getUiElement('error-message', self.element);
                if (!self.vars.errTextElem) {
                    shmi.log('[Numpad] no error-message element provided', 3);
                }
                self.vars.inputLimitMinimum = shmi.getUiElement('input-limit-minimum', self.element);
                if (!self.vars.inputLimitMinimum) {
                    shmi.log('[Numpad] no input-limit-minimum element provided', 3);
                }
                self.vars.inputLimitMinimumLabel = shmi.getUiElement('input-limit-minimum-label', self.element);
                if (!self.vars.inputLimitMinimumLabel) {
                    shmi.log('[Numpad] no input-limit-minimum-label element provided', 3);
                }
                self.vars.inputLimitMaximum = shmi.getUiElement('input-limit-maximum', self.element);
                if (!self.vars.inputLimitMaximum) {
                    shmi.log('[Numpad] no input-limit-maximum element provided', 3);
                }
                self.vars.inputLimitMaximumLabel = shmi.getUiElement('input-limit-maximum-label', self.element);
                if (!self.vars.inputLimitMaximumLabel) {
                    shmi.log('[Numpad] no input-limit-maximum-label element provided', 3);
                }
                self.vars.frameElement = shmi.getUiElement('numpad-frame', self.element);
                if (!self.vars.frameElement) {
                    shmi.log('[Numpad] no numpad-frame element provided', 3);
                }
                self.vars.inputDisplayElement = shmi.getUiElement('numpad-input-display', self.element);
                if (self.vars.inputDisplayElement) {
                    self.vars.inputDisplayElement.setAttribute("maxlength", self.config["max-chars"]);
                } else {
                    shmi.log('[Numpad] no numpad-input-display element provided', 3);
                }
                self.vars.unitElement = shmi.getUiElement('numpad-unit', self.element);
                if (!self.vars.unitElement) {
                    shmi.log('[Numpad] no numpad-unit element provided', 3);
                }
                Object.keys(self.vars.buttons).forEach(function(el) {
                    const elId = 'numpad-button-' + el;
                    self.vars.buttonsElements[el] = shmi.getUiElement(elId, self.element);
                    if (!self.vars.buttonsElements[el]) {
                        shmi.log(shmi.evalString(shmi.localize('[Numpad] no <%= elId %> element provided!'), { elId: elId }), 3);
                        return;
                    }
                    self.vars.buttonsElements[el].value = el;
                    self.vars.buttonsElements[el].setAttribute('tabindex', '1');
                });

                /* all required elements found */
                self.vars.visible = self.config['initially-visible'];
                if (self.vars.visible) {
                    shmi.addClass(self.element, 'open');
                }

                self.vars.containerElement = shmi.getUiElement('numpad-content', self.element);
                if (!self.vars.containerElement) {
                    shmi.log('[Numpad] no numpad-content element provided', 3);
                }

                if (self.vars.titleElement) {
                    if (self.config.label) {
                        self.vars.titleElement.textContent = shmi.localize(self.config.label);
                    }
                }
                if (self.vars.inputLimitMinimum) {
                    if (typeof self.config.min === "number") {
                        self.vars.inputLimitMinimum.textContent = self.config.min;
                    }
                }
                if (self.vars.inputLimitMinimumLabel) {
                    if (self.config.inputLimitMinimumLabel) {
                        self.vars.inputLimitMinimumLabel.textContent = shmi.localize(self.config.inputLimitMinimumLabel);
                    }
                }
                if (self.vars.inputLimitMaximum) {
                    if (typeof self.config.max === "number") {
                        self.vars.inputLimitMaximum.textContent = self.config.max;
                    }
                }
                if (self.vars.inputLimitMaximumLabel) {
                    if (self.config.inputLimitMaximumLabel) {
                        self.vars.inputLimitMaximumLabel.textContent = shmi.localize(self.config.inputLimitMaximumLabel);
                    }
                }
                if (self.vars.unitElement) {
                    if (self.config.unit && self.config.unit !== 'unit') {
                        self.vars.unitElement.textContent = (typeof self.config.unit === "number") ? self.config.unit : shmi.localize(self.config.unit);
                    } else {
                        shmi.addClass(self.vars.unitElement, 'hidden');
                    }
                }
                if (self.vars.inputDisplayElement) {
                    if (value(self)) {
                        self.vars.inputDisplayElement.value = String(value(self)).replace(".", self.config["decimal-delimiter"]);
                    }
                    self.vars.inputDisplayElement.onkeyup = function(e) {
                        if (e.key === "Enter") {
                            actionOk(self);
                        } else if (e.key === "Escape") {
                            actionClose(self);
                        } else {
                            value(self, self.vars.inputDisplayElement.value);
                        }
                    };
                }

                if (self.vars.buttonsElements["dot"]) {
                    self.vars.buttonsElements["dot"].textContent = shmi.localize("${DECIMAL_DELIMITER}");
                }

                // check for size of the numpad
                const inputElemClasses = self.vars.inputDisplayElement.className;
                if (inputElemClasses.indexOf('large') !== -1 || self.config['class-name'].indexOf('large') !== -1) {
                    self.vars.size = 'large';
                } else if (inputElemClasses.indexOf('small') !== -1 || self.config['class-name'].indexOf('small') !== -1) {
                    self.vars.size = 'small';
                } else {
                    self.vars.size = 'default';
                }

                // Key listeners for input field
                self.vars.inputDisplayListener = function(event) {
                    if (self.vars.keys.validKey.indexOf(event.which) !== -1) {
                        const currentValue = self.vars.inputDisplayElement.value;

                        // replace 0 if it is the only value
                        if (self.vars.keys.validNumber.indexOf(event.which) !== -1 && (currentValue === '0' || currentValue === '-0')) {
                            if (currentValue === '0') {
                                self.vars.inputDisplayElement.value = '';
                            } else if (currentValue === '-0') {
                                self.vars.inputDisplayElement.value = '-';
                            }
                            // listen for dash to convert value to negative value
                        } else if (self.vars.keys.minus.indexOf(event.which) !== -1) {
                            event.preventDefault();
                            // replace selection on input
                            // self.vars.inputDisplayElement.selectionStart = 0;
                            // self.vars.inputDisplayElement.selectionEnd = self.vars.inputDisplayElement.value.length;

                            if (currentValue.indexOf('-') === -1) {
                                self.vars.inputDisplayElement.value = '-' + currentValue;
                            }
                        } else if (self.vars.keys.plus.indexOf(event.which) !== -1) {
                            event.preventDefault();
                            if (currentValue.indexOf('-') !== -1) {
                                self.vars.inputDisplayElement.value = currentValue.replace('-', '');
                            }
                        }
                    } else if (!event.ctrlKey) { // allowing ctrl combinations prevent all others
                        event.preventDefault();
                    }
                };

                self.vars.elementEnterKey = function(event) {
                    if (event.key === "Enter") {
                        actionOk(self);
                    } else if (event.key === "Escape") {
                        actionClose(self);
                    }
                };

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                const numpadBackground = shmi.getUiElement('numpad-background', self.element),
                    numpadBox = shmi.getUiElement('numpad-content', self.element),
                    boxTitle = shmi.getUiElement('numpad-title', self.element);

                let initialX,
                    initialY,
                    initialNumpadPosition,
                    backgroundDim;

                const dragEventFunctions = {
                    onDrag: function(dx, dy, e) {
                        let currentX = initialNumpadPosition.x + e.clientX - initialX,
                            currentY = initialNumpadPosition.y + e.clientY - initialY;

                        currentX = Math.max(0, Math.min(backgroundDim.width - initialNumpadPosition.width, currentX));
                        currentY = Math.max(0, Math.min(backgroundDim.height - initialNumpadPosition.height, currentY));
                        numpadBox.style.left = `${currentX}px`;
                        numpadBox.style.top = `${currentY}px`;
                    },
                    onPress: function(posX, posY) {
                        initialX = posX;
                        initialY = posY;
                        initialNumpadPosition = numpadBox.getBoundingClientRect();
                        backgroundDim = numpadBackground.getBoundingClientRect();
                    }
                };

                self.vars.listeners.push(new shmi.visuals.io.MouseListener(boxTitle, dragEventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(boxTitle, dragEventFunctions));
            },
            /**
             * Displays the Numpad
             */
            show: function() {
                const self = this;
                if (!self.active) {
                    shmi.log("[Numpad] cannot show numpad without enabling it first", 3);
                    return;
                }
                if (self.vars.visible) {
                    return;
                }

                self.vars.visible = true;

                if (self.config['top-level'] === true) {
                    document.body.appendChild(self.element);
                }

                // Hide min max displays if not set
                if (!self.config['min'] || self.config['min'] === '') {
                    shmi.addClass(self.vars.inputLimitMinimumLabel.parentElement, 'no-min');
                } else {
                    shmi.removeClass(self.vars.inputLimitMinimumLabel.parentElement, 'no-min');
                }
                if (!self.config['max'] || self.config['max'] === '') {
                    shmi.addClass(self.vars.inputLimitMaximumLabel.parentElement, 'no-max');
                } else {
                    shmi.removeClass(self.vars.inputLimitMaximumLabel.parentElement, 'no-max');
                }

                if (self.config['cover-background']) {
                    if (self.vars.bgDiv) {
                        shmi.removeClass(self.vars.bgDiv, 'hidden');
                    } else {
                        self.vars.bgDiv = document.createElement('div');
                        shmi.addClass(self.vars.bgDiv, 'numpad-overlay');
                        self.vars.bgDiv.setAttribute('tabindex', '-1');
                        document.body.appendChild(self.vars.bgDiv);
                        shmi.removeClass(self.vars.bgDiv, 'hidden');
                        const bgFuncs = {};
                        bgFuncs.onPress = function() {
                            document.activeElement.blur();
                        };
                        shmi.visuals.io.TouchListener(self.vars.bgDiv, bgFuncs);
                    }
                }

                shmi.addClass(self.element, 'open');
                setTimeout(function() {
                    self.vars.inputDisplayElement.focus();
                    self.fire('open', {});
                }, shmi.c("DECOUPLE_TIMEOUT"));

                self.vars.inputDisplayElement.selectionStart = 0;
                self.vars.inputDisplayElement.selectionEnd = self.vars.inputDisplayElement.value.length;
            },
            /**
             * Removes the Numpad from DOM
             *
             */
            hide: function() {
                const self = this;
                if (!self.vars.visible) {
                    return;
                }
                self.vars.visible = false;
                shmi.removeClass(self.element, 'open');
                // self.reset();
                if (self.config['cover-background']) {
                    if (self.vars.bgDiv) {
                        shmi.addClass(self.vars.bgDiv, 'hidden');
                    }
                }
                self.fire('close', {});
            },
            /**
             * Enables the Numpad
             */
            onEnable: function() {
                const self = this;
                let ml,
                    tl;

                // Mouse and touch listeners
                const functions = {};
                functions.onPress = function(x, y, event) {
                    const el = event.target;
                    shmi.addClass(self.element, 'pressed');
                    shmi.log("[Numpad] pressed", 1);

                    onNumpadAction(el.value, self, el);
                };
                functions.onRelease = function() {
                    shmi.removeClass(self.element, 'pressed');
                    shmi.log("[Numpad] released", 1);
                };
                ml = new shmi.visuals.io.MouseListener(self.vars.containerElement, functions);
                tl = new shmi.visuals.io.TouchListener(self.vars.containerElement, functions, true);

                self.vars.listeners.push(ml, tl);

                self.element.setAttribute('tabindex', '1');

                // key listeners for the "ok" and "cancel" actions
                self.element.addEventListener('keydown', self.vars.elementEnterKey);

                self.vars.inputDisplayElement.addEventListener('keydown', self.vars.inputDisplayListener);

                //--------------------
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.log("[Numpad] enabled", 1);
            },
            /**
             * Disables the Numpad
             *
             */
            onDisable: function() {
                const self = this;
                self.hide();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.vars.elementEnterKey) {
                    self.element.removeEventListener('keydown', self.vars.elementEnterKey);
                }

                if (self.vars.inputDisplayElement) {
                    self.vars.inputDisplayElement.removeEventListener('keydown', self.vars.inputDisplayListener);
                }

                // remove overlay background element if present
                if (self.vars.bgDiv) {
                    self.vars.bgDiv.parentNode.removeChild(self.vars.bgDiv);
                    self.vars.bgDiv = null;
                }

                shmi.log("[Numpad] disabled", 1);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(val, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };
    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

(function() {
    shmi.pkg("visuals.controls");
    /**
     * Creates a new shmi.visuals.session.Layout Panel control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {HTMLElement} element root element of the control
     * @param {Object} config control configuration
     * @param {Boolean} active true if panel should always be active, false else
     */
    shmi.visuals.controls.Panel = function(element, config) {
        /* check for required packages */
        shmi.requires("visuals.controls.View");
        shmi.requires("visuals.controls.panel.PanelIndicator");
        shmi.requires("visuals.gfx.Animation");
        shmi.requires("visuals.gfx.Movable");
        shmi.requires("visuals.io.MouseListener");
        shmi.requires("visuals.io.TouchListener");

        this.element = element;
        this.configName = this.element.getAttribute('data-config-name');

        if (arguments.length === 3) {
            this.config = arguments[2] || {};
        } else {
            this.config = config || {};
        }

        this.parseAttributes();

        shmi.def(this.config, 'class-name', 'panel');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'show-indicator', false);
        shmi.def(this.config, 'disable-swipe', false);
        shmi.def(this.config, 'anim-duration', 250);
        shmi.def(this.config, 'item', null);
        shmi.def(this.config, 'animate', false);
        shmi.def(this.config, 'initial-view', 0);

        /* convert text 'true' to boolean 'true' */
        this.config.animate = (this.config.animate === "true") || (this.config.animate === true);

        this.viewElements = [];
        this.controls = [];
        this.subpanels = [];
        this.history = [];
        this.currentView = 0;
        this.nextView = 0;
        this.initialized = false;
        this._dragging = false;
        this.anim = new shmi.visuals.gfx.Animation(this.updatePosition.bind(this), this.updateVisibility.bind(this), "ease_out");
        this.anim.priority = true;

        this.clientWidth = null;
        this.clientHeight = null;
        this.ignoreResize = false;
        this.resizeTO = 0;
        this.resizeObserver = null;

        this.active = false;

        this.startup();
    };

    function initializeViews(self) {
        var views = shmi.getUiElements('view', self.element);
        for (var i = 0; i < views.length; i++) {
            if (views[i].parentNode === self.element) {
                views[i].setAttribute('tabindex', '-1');
                self.viewElements.push(views[i]);
            }
        }

        self.clientWidth = self.element.clientWidth;
        self.clientHeight = self.element.clientHeight;
        self.contentDiv.style.width = (self.clientWidth * self.viewElements.length) + 'px';

        self.viewWidth = self.clientWidth;

        for (i = 0; i < self.viewElements.length; i++) {
            var div = document.createElement('div');
            div.classList.add("iqel-view-wrapper");
            div.style.width = self.clientWidth + 'px';
            div.appendChild(self.viewElements[i]);
            self.contentDiv.appendChild(div);
        }
    }

    function notifyResize(self) {
        const evt = document.createEvent("Event");

        self.ignoreResize = true;
        evt.initEvent("resize", true, true);
        window.dispatchEvent(evt);
        self.ignoreResize = false;
    }

    shmi.visuals.controls.Panel.prototype = {
        uiType: "panel",
        isContainer: true,
        events: ["navigate"],
        getClassName: function() {
            return "Panel";
        },
        onRegister: function(onDone) {
            var self = this;
            /* register event listener id */
            if (self.config['show-indicator'] === "true") {
                self.config['show-indicator'] = true;
            } else if (self.config['show-indicator'] === "false") {
                self.config['show-indicator'] = false;
            }

            if (self.config['disable-swipe'] === "true") {
                self.config['disable-swipe'] = true;
            } else if (self.config['disable-swipe'] === "false") {
                self.config['disable-swipe'] = false;
            }

            self.contentDiv = document.createElement('div');
            self.contentDiv.classList.add("iqel-transform-container");
            self.element.appendChild(self.contentDiv);
            initializeViews(self);
            self.parseChildren(self.contentDiv, onDone);
        },
        onInit: function() {
            var self = this;

            this.mover = new shmi.visuals.gfx.Movable(this.contentDiv);
            this.mover.rotatable = false;
            this.mover.scalable = false;
            this.mover.priority = true;

            this.resizeObserver = new ResizeObserver(() => {
                if (self.ignoreResize) {
                    return;
                }
                clearTimeout(self.resizeTO);
                self.resizeTO = setTimeout(function() {
                    self.updateSize();
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            });

            if (!this.config['disable-swipe']) {
                var divFuncs = {};
                divFuncs.onDrag = function(dx, dy, event) {
                    var abs_dy = Math.abs(dy),
                        abs_dx = Math.abs(dx);

                    if (abs_dx > abs_dy) {
                        event.preventDefault();
                        if ((this.mover.tx + dx) < (-this.clientWidth * (this.viewElements.length - 1))) {
                            this.mover.translate(dx / 5, 0);
                        } else if ((this.mover.tx + dx) > 0) {
                            this.mover.translate(dx / 5, 0);
                        } else {
                            this.mover.translate(dx, 0);
                        }
                        if (!this._dragging) {
                            this._dragging = true;
                            if (document.activeElement !== document.body) {
                                document.activeElement.blur();
                            }
                            this.showNeighbors();
                        }
                    }
                }.bind(this);
                divFuncs.onPress = function() {
                    this.updateSize();
                    this.mover.setTransition(false);
                    if (document.activeElement !== document.body) {
                        try {
                            document.activeElement.blur();
                        } catch (exc) { /* blur not supported, probably SVGElement */ }
                    }
                }.bind(this);
                divFuncs.onRelease = function() {
                    if (this._dragging) {
                        this._dragging = false;
                        var oldView = this.currentView,
                            nextView = this.currentView,
                            diff = (this.currentView * this.viewWidth) + (this.mover.tx);
                        if (diff > (0.2 * this.viewWidth)) {
                            nextView = this.currentView - 1;
                        } else if (diff < (-0.2 * this.viewWidth)) {
                            nextView = this.currentView + 1;
                        }
                        if ((nextView >= 0) && (nextView < this.viewElements.length)) {
                            if (this.currentView !== nextView) {
                                this.nextView = nextView;
                                this.history.push(this.currentView);
                            }
                            this.currentView = nextView;
                            /* write current view index to item if configured */
                            if (this.config.item) {
                                shmi.visuals.session.ItemManager.writeValue(this.config.item, nextView);
                            }
                            if (!this.navInited && this.config['nav-name']) {
                                this.navInited = true;
                                this.nav = shmi.getControlByName(this.config['nav-name']);
                            }
                            if (this.nav) {
                                this.nav.setActive(this.currentView);
                            }
                        }
                        var dest = -1 * this.currentView * this.viewWidth,
                            newView = this.controls[this.currentView],
                            eventDetail = {
                                index: this.currentView,
                                old_index: oldView
                            };

                        if (self.isActive()) {
                            if (self.controls[oldView] && self.controls[oldView].isActive()) {
                                self.controls[oldView].disable();
                            }
                            newView.enable();
                            this.fire('navigate', eventDetail);
                        }
                        this.anim.setCurrentValue(this.mover.tx);
                        this.anim.start(dest, this.config['anim-duration']);
                    }
                }.bind(this);

                this.mouseListener = new shmi.visuals.io.MouseListener(this.contentDiv, divFuncs);
                this.touchListener = new shmi.visuals.io.TouchListener(this.contentDiv, divFuncs);
                this.touchListener.prevent_drag_scroll_y = false;
            }
            if (this.config['show-indicator']) {
                this.indicator = new shmi.visuals.controls.panel.PanelIndicator(this);
                shmi.addClass(this.element, "show-indicator");
            }
            this.updateVisibility();
            if (self.viewElements.length > self.config['initial-view'] && (self.currentView === self.nextView)) {
                self.setView(self.config['initial-view']);
            }
            shmi.log("[Panel] " + this.config.name + " created", shmi.c('LOG_INFO'));
        },
        /**
         * Updates visibility of panel views
         *
         */
        updateVisibility: function() {
            shmi.log("[Panel] update visibility -  start", shmi.c('LOG_DEBUG'));
            for (var i = 0; i < this.viewElements.length && i < this.controls.length; i++) {
                if (i !== this.currentView) {
                    shmi.addClass(this.viewElements[i], 'hidden');
                } else {
                    shmi.removeClass(this.viewElements[i], 'hidden');
                }
            }
            if (this.config['show-indicator']) {
                this.indicator.setSelected(this.currentView);
            }
            shmi.log("[Panel] update visibility -  end", shmi.c('LOG_DEBUG'));
        },
        /**
         * Updates translation of panel content
         *
         */
        updatePosition: function(val) {
            this.mover.tx = val;
            this.mover.draw();
        },
        /**
         * Called when switching views has finished
         *
         */
        transitionComplete: function() {
            this.updateVisibility();
        },
        /**
         * Shows neighboring views of current view
         *
         */
        showNeighbors: function() {
            if (((this.currentView - 1) >= 0)) {
                shmi.removeClass(this.viewElements[this.currentView - 1], 'hidden');
            }
            if ((this.currentView + 1) < this.viewElements.length) {
                shmi.removeClass(this.viewElements[this.currentView + 1], 'hidden');
            }
        },
        /**
         * Sets active view to specified index
         *
         * @param viewindex - index of new active view
         */
        setView: function(viewindex) {
            var self = this;

            if ((viewindex < 0) || (viewindex >= this.viewElements.length)) {
                viewindex = 0;
            }

            if (viewindex !== Number(this.currentView)) {
                this.nextView = viewindex;
                if (!this.initialized) {
                    return;
                }
                if (!this.backPushed) {
                    this.history.push(this.currentView);
                } else {
                    this.backPushed = false;
                }
            } else {
                return;
            /* don't do anything if currently active view is set again */
            }
            var lastView = this.currentView;
            this.currentView = viewindex;
            if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, viewindex);
            }
            var newView = this.controls[viewindex],
                e_detail = {
                    index: viewindex,
                    old_index: lastView
                };

            shmi.removeClass(this.viewElements[this.currentView], 'hidden');
            if (self.isActive()) {
                if (self.controls[lastView] && self.controls[lastView].isActive()) {
                    self.controls[lastView].disable();
                }
                newView.enable();
                this.fire('navigate', e_detail);
            }
            var dest = -1 * this.currentView * this.viewWidth;
            this.anim.setCurrentValue(this.mover.tx);
            if (this.config.animate === true) {
                this.anim.start(dest, this.config['anim-duration']);
            } else {
                this.anim.start(dest, 0);
            }
            shmi.log("[Panel '" + this.config.name + "'] view " + viewindex + " set", shmi.c('LOG_INFO'));
        },
        back: function() {
            if (this.history.length > 0) {
                this.backPushed = true;
                this.setView(this.history.pop());
            } else {
                shmi.log("[Panel] history is empty, cannot go back", shmi.c('LOG_INFO'));
            }
        },
        /**
         * Enables the Panel
         *
         */
        onEnable: function() {
            const self = this;

            self.updateSize();
            self.resizeObserver.observe(self.element);

            if (!self.config['disable-swipe'] && self.mouseListener && self.touchListener) {
                self.mouseListener.enable();
                self.touchListener.enable();
            }

            if (self.config.item) {
                self._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(self.config.item, self);
            }

            if (self.currentView !== self.nextView) {
                self.setView(self.nextView);
            } else if (self.controls[self.currentView]) {
                self.controls[self.currentView].enable();
            }
        },
        /**
         * Disables the Panel
         *
         */
        onDisable: function() {
            if (this.controls[this.currentView] !== undefined) {
                this.controls[this.currentView].disable();
            }

            if (!this.config['disable-swipe'] && this.mouseListener && this.touchListener) {
                this.mouseListener.disable();
                this.touchListener.disable();
            }

            if (this.config.item) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
            }
            this.resizeObserver.unobserve(this.element);
            this.anim.stop();
        },
        onLock: function() {
            var self = this;

            if (!self.config['disable-swipe'] && self.mouseListener && self.touchListener) {
                this.mouseListener.disable();
                this.touchListener.disable();
            }

            if (self.indicator) {
                self.indicator.lock();
            }

            self.controls.forEach(function(ctrl) {
                ctrl.lock();
            });

            shmi.addClass(self.element, "locked");
        },
        onUnlock: function() {
            var self = this;

            if (!self.config['disable-swipe'] && self.mouseListener && self.touchListener && self.active) {
                self.mouseListener.enable();
                self.touchListener.enable();
            }

            if (self.indicator && self.active) {
                self.indicator.unlock();
            }

            self.controls.forEach(function(ctrl) {
                ctrl.unlock();
            });

            shmi.removeClass(self.element, "locked");
        },
        onSetValue: function(value) {
            if (shmi.isDesignerEditor()) {
                return;
            }
            value = parseInt(value);
            if (!isNaN(value)) {
                this.setView(value);
            }
        },
        updateSize: function() {
            var self = this;
            if (!self.active) {
                return;
            }
            /* cancel execution */

            shmi.log("[Panel] resizing panel...", shmi.c('LOG_DEBUG'));
            self.clientWidth = self.element.clientWidth;
            self.clientHeight = self.element.clientHeight;
            if (!self.contentDiv) {
                return;
            }
            self.contentDiv.style.width = (self.clientWidth * self.viewElements.length) + "px";

            self.viewWidth = self.clientWidth;
            for (var i = 0; i < self.viewElements.length; i++) {
                var div = self.viewElements[i].parentNode;
                div.style.width = self.clientWidth + "px";
            }

            var dest = -1 * self.currentView * self.viewWidth;
            self.anim.setCurrentValue(dest);
            self.updateVisibility();
            self.updatePosition(dest);
            notifyResize(self);
        },
        onDelete: function() {
            this.anim.stop();
        },
        onAddControl: function(options, callback) {
            var self = this,
                cm = shmi.requires("visuals.tools.controller"),
                controls = [],
                notView = false,
                initToken = null;

            if (!self.initialized) {
                throw new Error("Control not initialized yet.");
            }

            if (!Array.isArray(options)) {
                options = [options];
            }

            notView = options.some(function(option) {
                return (option.ui !== "view");
            });

            if (notView) {
                callback(new Error("All children must be of type 'view'"), controls);
                return;
            }

            options.forEach(function(option) {
                var control = null,
                    viewDiv = document.createElement('div');

                viewDiv.classList.add("iqel-view-wrapper");
                viewDiv.style.width = self.clientWidth + 'px';
                self.contentDiv.appendChild(viewDiv);

                option.config.index = self.controls.length;
                control = shmi.createControl(option.ui, viewDiv, option.config, "DIV", null, false);

                if (control !== null) {
                    if (option.style && typeof option.style === "object") {
                        let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                        iter(option.style, (value, name) => {
                            control.element.style[name] = value;
                        });
                    }
                    shmi.addClass(control.element, "hidden");
                    self.viewElements.push(control.element);
                    controls.push(control);
                }
            });
            self.updateSize();

            initToken = shmi.waitOnInit(controls, function() {
                var idx = -1,
                    errors = [],
                    tm = shmi.requires("visuals.task"),
                    tasks = [],
                    tl = null;

                if (initToken) {
                    idx = self._init_.tokens.indexOf(initToken);
                    if (idx !== -1) {
                        self._init_.tokens.splice(idx, 1);
                    }
                    initToken.unlisten();
                    initToken = null;
                }

                options.forEach(function(opt, jdx) {
                    var t = null,
                        control = controls[jdx];

                    if (opt.controller) {
                        cm.create(opt.controller.name, opt.controller, control.getName());
                    }

                    if (Array.isArray(opt.children) && opt.children.length) {
                        t = tm.createTask("child control");
                        t.run = function() {
                            control.addControl(opt.children, function(childErr, childOptions) {
                                if (childErr) {
                                    errors.push(childErr);
                                }
                                t.complete();
                            });
                        };
                        tasks.push(t);
                    }
                });

                if (tasks.length) {
                    tl = tm.createTaskList(tasks, false);
                    tl.onComplete = function() {
                        if (self.controls.length && self.controls.length === controls.length) {
                            self.setView(self.config["initial-view"]);
                        }
                        if (self.indicator) {
                            self.indicator.update();
                        }
                        callback(errors.length ? errors[0] : null, controls);
                    };
                    tl.run();
                } else {
                    if (self.controls.length && self.controls.length === controls.length) {
                        self.setView(self.config["initial-view"]);
                    }
                    if (self.indicator) {
                        self.indicator.update();
                    }
                    callback(errors.length ? errors[0] : null, controls);
                }
            });

            if (initToken) {
                self._init_.tokens.push(initToken);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.Panel, shmi.visuals.core.BaseControl);
}());

shmi.pkg("visuals.controls.panel");
/**
 * View Indicator for Panel Control
 *
 * @constructor
 * @param panel - panel to indicate views of
 */
shmi.visuals.controls.panel.PanelIndicator = function(panel) {
    const indicatorOuter = document.createElement("div"),
        indicatorInner = document.createElement("div"),
        stateLayer = document.createElement("div"),
        indicatorBase = document.createElement("div");

    if (panel === undefined) {
        shmi.log("[PanelIndicator] panel not found", 3);
        return;
    }

    this.panel = panel;
    this.template = document.createElement('div');

    indicatorBase.classList.add("iqel-indicator");
    indicatorBase.setAttribute("data-ui", "indicator");
    stateLayer.classList.add("iqel-state-layer");
    indicatorOuter.classList.add("iqel-indicator-marker");
    indicatorInner.classList.add("iqel-indicator-graphic");
    stateLayer.appendChild(indicatorOuter);
    indicatorOuter.appendChild(indicatorInner);
    indicatorBase.appendChild(stateLayer);

    this.template.appendChild(indicatorBase);
    this.element = document.createElement('div');
    this.element.setAttribute('data-ui', 'panel-indicator');
    this.indicatorElements = [];
    this.listeners = [];
    this.selected = 0;
    this.init();
};

shmi.visuals.controls.panel.PanelIndicator.prototype = {
    update: function() {
        const self = this;

        self.listeners.forEach(function(l) {
            l.disable();
        });

        self.listeners = [];
        self.indicatorElements = [];

        if (self.element.parentNode === self.panel.element) {
            self.panel.element.removeChild(self.element);
        }

        self.element.innerHTML = "";
        self.init();
        self.setSelected(self.selected);
    },
    init: function() {
        const self = this;

        self.indicator = shmi.getUiElement('indicator', this.template);
        if (!self.indicator) {
            shmi.log("[PanelIndicator] indicator element not found in template", 3);
            return;
        }

        self.listeners.forEach(function(l) {
            l.disable();
        });
        self.listeners = [];

        self.panel.controls.forEach(function(view, idx) {
            const indicatorElement = self.indicator.cloneNode(true),
                io = shmi.requires("visuals.io");

            self.element.appendChild(indicatorElement);
            self.indicatorElements.push(indicatorElement);

            const indicatorHandler = {
                onClick: function() {
                    self.panel.setView(idx);
                }
            };
            self.listeners.push(new io.MouseListener(indicatorElement, indicatorHandler));
            self.listeners.push(new io.TouchListener(indicatorElement, indicatorHandler));

            if (idx === Number(self.panel.currentView)) {
                shmi.addClass(indicatorElement, 'active');
                self.selected = idx;
            }
        });

        self.panel.listen('enable', function() {
            self.listeners.forEach(function(l) {
                l.enable();
            });
        });
        self.panel.listen('disable', function() {
            self.listeners.forEach(function(l) {
                l.disable();
            });
        });

        if (self.panel.isActive()) {
            self.listeners.forEach(function(l) {
                l.enable();
            });
        }

        self.panel.element.appendChild(self.element);
        shmi.addClass(this.element, 'iqel-indicator-container');
    },
    /**
     * Sets active view to selected index
     *
     * @param index - index of active view
     */
    setSelected: function(index) {
        this.selected = Number(index);
        for (let i = 0; i < this.indicatorElements.length; i++) {
            if (i === index) {
                shmi.addClass(this.indicatorElements[i], 'active');
            } else {
                shmi.removeClass(this.indicatorElements[i], 'active');
            }
        }
    },
    lock: function() {
        const self = this;

        self.listeners.forEach(function(lst) {
            lst.disable();
        });
    },
    unlock: function() {
        const self = this;

        self.listeners.forEach(function(lst) {
            lst.enable();
        });
    }
};

(function() {
    shmi.pkg("visuals.controls");
    /**
     * Popup Menu Control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param element - the base element of the control
     * @param config - configuration of the control
     */
    shmi.visuals.controls.PopupMenu = function(element, config) {
        this.element = element;

        this.config = config || {};

        this.parseAttributes();

        /*set default options */
        shmi.def(this.config, 'class-name', 'popup-menu');
        shmi.def(this.config, 'template', 'default/popup-menu');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'content-template', null);
        shmi.def(this.config, 'auto-close', false);
        shmi.def(this.config, 'def-auto-close', true);
        shmi.def(this.config, 'moveable', false);
        shmi.def(this.config, 'tab-limit', true);
        shmi.def(this.config, 'close-icon-src', "pics/system/controls/dialog-box/close.svg");
        shmi.def(this.config, 'show-close-button', false);

        if (this.config['auto-close'] === "false") {
            this.config['auto-close'] = false;
        }

        this.controls = [];
        this.opened = false;
        this.locked = false;
        this.cls = [];
        /* click listener info objects for child controls */
        this.listeners = [];
        this.tokens = [];
        this.menu_background = null;
        this.menu_content = null;
        this.closeButtons = [];

        this.startup();
    };

    /**
     * Find the nearest parent element that has valid dimensions (width and height < 0)
     *
     * @param {HTMLElement} element The element to start the search from.
     */
    function findContainerWithBounds(element) {
        while (element) {
            const rect = element.getBoundingClientRect();
            if (rect.width > 0 && rect.height > 0) {
                return { element, rect };
            }
            element = element.parentElement;
        }
        return null;
    }

    shmi.visuals.controls.PopupMenu.prototype = {
        uiType: "popup-menu",
        events: ['open', 'close'],
        isContainer: true,
        getClassName: function() {
            return "PopupMenu";
        },
        onRegister: function(onDone) {
            const self = this;

            shmi.addClass(self.element, 'hidden');
            self.menu_background = shmi.getUiElement('menu-background', self.element);
            self.menu_content = shmi.getUiElement('menu-content', self.element);

            if (self.config['content-template']) {
                const templateUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                    self.config["content-template"] :
                    shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");

                shmi.loadResource(templateUrl, function(response, failed, url) {
                    if (self.isDeleted()) {
                        return;
                    }
                    if (!failed && self.menu_content) {
                        self.menu_content.innerHTML = response;
                        self.parseChildren(self.menu_content, onDone);
                    } else {
                        onDone();
                    }
                });
            } else if (self.menu_content) {
                self.parseChildren(self.menu_content, onDone);
            } else {
                onDone();
            }
        },
        /**
         * Initializes the control.
         *
         */
        onInit: function() {
            const self = this,
                inl = shmi.requires("visuals.tools.iterate.iterateNodeList");
            let closeButtons = [];

            if (self.menu_content) {
                closeButtons = shmi.getUiElements("iq-button", self.element);
                inl(closeButtons, function(el) {
                    let but = null;
                    if (el.parentNode && !shmi.testParentChild(self.menu_content, el)) {
                        but = shmi.createControl("iq-button", el, {
                            "show-icon": true,
                            "show-text": false,
                            "icon-src": self.config["close-icon-src"] ?? "pics/system/controls/dialog-box/close.svg",
                            "inline-svg-icon": true,
                            "label": null
                        }, "DIV", "from");
                        self.closeButtons.push(but);
                    }
                });
            }

            // If the 'show-close-button' config is set, either show or hide the
            // close button, depending on the configuration. If it's unset, leave
            // the css class alone.
            if (this.config['show-close-button']) {
                this.element.classList.add('show-close-button');
            } else if (!this.config['show-close-button']) {
                this.element.classList.remove('show-close-button');
            }

            if (this.config['moveable']) {
                this.element.classList.add('show-drag-handle');
            } else {
                this.element.classList.remove('show-drag-handle');
            }

            if (!this.menu_background) {
                shmi.notify("${V_MISSING_UI_ELEMENT} 'menu-background'", "${V_ERROR}");
            } else if (self.config['def-auto-close']) {
                const funcs = {
                    onClick: function(x, y, event) {
                        self.close();
                    }
                };
                const io = shmi.visuals.io,
                    ml = new io.MouseListener(this.menu_background, funcs),
                    tl = new io.TouchListener(this.menu_background, funcs);
                this.listeners.push(ml);
                this.listeners.push(tl);
            } else {
                this.menu_background.style.pointerEvents = 'none';
            }

            if (!this.menu_content) {
                shmi.notify("${V_MISSING_UI_ELEMENT} 'menu-content'", "${V_ERROR}");
            }

            /*****************/
            /*** LISTENERS ***/
            /*****************/

            if (self.config['moveable']) {
                const menuDragBar = shmi.getUiElement('menu-drag-bar', self.element),
                    menuBackground = shmi.getUiElement('menu-background', self.element),
                    menuFrame = shmi.getUiElement('menu-frame', self.element);
                menuDragBar.style.display = 'block';

                let initialX,
                    initialY,
                    initialMenuPosition,
                    menuBackgroundRect,
                    containerRect;

                const dragEventFunctions = {
                    onDrag: function(dx, dy, e) {
                        const clientX = e.changedTouches ? e.changedTouches[0].clientX : e.clientX,
                            clientY = e.changedTouches ? e.changedTouches[0].clientY : e.clientY,
                            offsetY = menuBackgroundRect.top - containerRect.rect.top;
                        let currentX = initialMenuPosition.x + clientX - initialX - containerRect.rect.left,
                            currentY = initialMenuPosition.y + clientY - initialY - containerRect.rect.top;

                        currentX = Math.max(0, Math.min(containerRect.rect.width - initialMenuPosition.width, currentX));
                        currentY = Math.max(offsetY, Math.min(menuBackgroundRect.height - initialMenuPosition.height + offsetY, currentY));
                        self.element.style.setProperty('left', `${currentX}px`, 'important');
                        self.element.style.setProperty('top', `${currentY}px`, 'important');
                    },
                    onPress: function(posX, posY) {
                        initialX = posX;
                        initialY = posY;
                        initialMenuPosition = menuFrame.getBoundingClientRect();
                        menuBackgroundRect = menuBackground.getBoundingClientRect();
                        containerRect = findContainerWithBounds(self.element.offsetParent || document.documentElement);
                    }
                };

                self.listeners.push(new shmi.visuals.io.MouseListener(menuDragBar, dragEventFunctions));
                self.listeners.push(new shmi.visuals.io.TouchListener(menuDragBar, dragEventFunctions));
            }
        },
        onEnable: function() {
            const self = this;
            for (let i = 0; i < self.listeners.length; i++) {
                self.listeners[i].enable();
            }
            if (self.config['auto-close']) {
                self.tokens.push(shmi.listen("click", function(evt) {
                    if (evt.source && evt.source.element) {
                        if (shmi.testParentChild(self.element, evt.source.element)) {
                            self.close();
                        }
                    }
                }));
            }
            self.tokens.push(
                ...self.closeButtons.map(
                    (button) => button.listen("click", () => self.close())
                )
            );
        },
        onDisable: function() {
            const self = this;
            self.close();
            for (let i = 0; i < self.listeners.length; i++) {
                self.listeners[i].disable();
            }
            self.tokens.forEach(function(t) {
                t.unlisten();
            });
            self.tokens = [];
        },
        onSetValue: function() {

        },
        onLock: function() {
            const self = this;
            self.listeners.forEach(function(l) {
                l.disable();
            });
            self.locked = true;
        },
        onUnlock: function() {
            const self = this;
            self.listeners.forEach(function(l) {
                l.enable();
            });
            self.locked = false;
        },
        open: function() {
            const self = this;
            if (self.active && (!self.opened)) {
                self.opened = true;
                shmi.removeClass(self.element, 'hidden');

                if (self.config['tab-limit']) {
                    // Enable element tabulator
                    const tabulator = shmi.requires("visuals.tools.tabulator");
                    self.tabLimit = tabulator.setTabParent(self.element);
                }

                for (let i = 0; i < self.controls.length; i++) {
                    self.controls[i].enable();
                }
                self.fire('open', {});

                if (self.config['moveable'] && self.config['show-close-button']) {
                    const menuHeader = shmi.getUiElement('menu-header', self.element);
                    if (menuHeader) {
                        const dragBar = shmi.getUiElement('menu-drag-bar', menuHeader),
                            closeButton = shmi.getUiElement('iq-button', menuHeader);
                        if (dragBar && closeButton) {
                            menuHeader.style.gridTemplateColumns = `${closeButton.offsetWidth}px auto ${closeButton.offsetWidth}px`;
                            if (closeButton.offsetWidth * 2 + dragBar.offsetWidth > menuHeader.offsetWidth) {
                                dragBar.style.paddingLeft = '0';
                                dragBar.style.paddingRight = '0';
                            }
                        }
                    }
                }
            }
        },
        close: function() {
            if (this.opened) {
                this.opened = false;
                shmi.addClass(this.element, 'hidden');
                this.fire('close', {});
                for (let i = 0; i < this.controls.length; i++) {
                    if (this.tabLimit) {
                        this.tabLimit.unsetTabParent();
                        this.tabLimit = null;
                    }
                    this.controls[i].disable();
                }
            }
        },
        toggle: function() {
            const self = this;
            if (self.opened) {
                self.close();
            } else {
                self.open();
            }
        },
        onAddControl: function(options, callback) {
            const self = this,
                cm = shmi.requires("visuals.tools.controller"),
                controls = [];
            let initToken = null;

            if (!self.initialized) {
                throw new Error("Control not initialized yet.");
            }

            if (!Array.isArray(options)) {
                options = [options];
            }
            options.forEach(function(option) {
                const control = shmi.createControl(option.ui, self.menu_content, option.config, "DIV", null, false);
                if (control !== null) {
                    if (option.style && typeof option.style === "object") {
                        let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                        iter(option.style, (value, name) => {
                            control.element.style[name] = value;
                        });
                    }
                    controls.push(control);
                }
            });
            initToken = shmi.waitOnInit(controls, function() {
                var idx = -1,
                    errors = [],
                    tm = shmi.requires("visuals.task"),
                    tasks = [],
                    tl = null;

                if (initToken) {
                    idx = self._init_.tokens.indexOf(initToken);
                    if (idx !== -1) {
                        self._init_.tokens.splice(idx, 1);
                    }
                    initToken.unlisten();
                    initToken = null;
                }

                options.forEach(function(opt, jdx) {
                    let t = null,
                        control = controls[jdx];

                    if (opt.controller) {
                        cm.create(opt.controller.name, opt.controller, control.getName());
                    }
                    if (self.isActive() && self.opened) {
                        control.enable();
                    }
                    if (Array.isArray(opt.children) && opt.children.length) {
                        t = tm.createTask("child control");
                        t.run = function() {
                            control.addControl(opt.children, function(childErr, childOptions) {
                                if (childErr) {
                                    errors.push(childErr);
                                }
                                t.complete();
                            });
                        };
                        tasks.push(t);
                    }
                });

                if (tasks.length) {
                    tl = tm.createTaskList(tasks, false);
                    tl.onComplete = function() {
                        callback(errors.length ? errors[0] : null, controls);
                    };
                    tl.run();
                } else {
                    callback(errors.length ? errors[0] : null, controls);
                }
            });
            if (initToken) {
                self._init_.tokens.push(initToken);
            }
        }
    };

    /* Define Ui-Action open / close for PopupMenu */
    shmi.visuals.session.userActions = shmi.visuals.session.userActions || {};
    shmi.visuals.session.userActions['popup-menu'] = function(params, param1, param2, param3, owner) {
        if (params.length < 2) {
            shmi.log("[UserAction] popup-menu - needs two parameters", 2);
        } else {
            let control;
            if ((params.length === 3) && (params[2] === "relative")) {
                control = shmi.ctrl(params[0], owner);
            } else {
                control = shmi.ctrl(params[0]);
            }
            if (control) {
                try {
                    if (params[1] === "open") {
                        control.open();
                        shmi.log("[UserAction] popup-menu - opened", 2);
                    } else if (params[1] === "close") {
                        control.close();
                        shmi.log("[UserAction] popup-menu - closed", 2);
                    } else if (params[1] === "toggle") {
                        control.toggle();
                        shmi.log("[UserAction] popup-menu - toggled", 2);
                    }
                } catch (exc) {
                    shmi.log("[UserAction] exception opening / closing popup-menu: " + exc, 2);
                }
            } else {
                shmi.log("[UserAction] popup-menu - no element found by name '" + params[0] + "'", 2);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.PopupMenu, shmi.visuals.core.BaseControl);
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new control ProjectWindow
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control project-window
 * @param config - configuration of the control project-window
 */
shmi.visuals.controls.ProjectWindow = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'project-window');
    shmi.def(this.config, 'template', 'default/project-window');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'initial-state', 'open');
    shmi.def(this.config, 'label', 'LABEL');
    shmi.def(this.config, 'alt-label', null);
    shmi.def(this.config, 'content-template', null);

    this._open = false;
    this.initialized = false;
    this.contentTemplate = null;
    /* storage for content template data */
    this.contentWrapper = null;
    /* content wrapper element */
    this.controls = [];
    this.listeners = [];

    this.startup();
};

shmi.visuals.controls.ProjectWindow.prototype = {
    uiType: "project-window",
    isContainer: true,
    getClassName: function() {
        return "ProjectWindow";
    },
    onRegister: function(onDone) {
        var self = this;

        self.contentElement = shmi.getUiElement('content', self.element);
        self.contentWrapper = shmi.getUiElement('content-wrapper', self.element);

        if (self.config['content-template']) {
            var contentUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                self.config["content-template"] :
                shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");

            shmi.loadResource(contentUrl, function(response, failed, url) {
                if (self.isDeleted()) {
                    return;
                }
                if (!failed && self.contentElement) {
                    self.contentElement.innerHTML = response;
                    self.parseChildren(self.contentElement, onDone);
                } else {
                    onDone();
                }
            });
        } else if (self.contentElement) {
            self.parseChildren(self.contentElement, onDone);
        } else {
            onDone();
        }
    },
    /**
     * Initialize control
     */
    onInit: function() {
        var self = this,
            lbl_el = shmi.getUiElement('label', self.element),
            head_el = shmi.getUiElement('header', self.element),
            altLabel = shmi.getUiElement('label-2', self.element);

        if (altLabel && self.config["alt-label"]) {
            altLabel.textContent = shmi.localize(self.config["alt-label"]);
        }

        if (lbl_el && head_el) {
            lbl_el.textContent = shmi.localize(self.config.label);

            var funcs = {
                onClick: function(x, y, events) {
                    if (self._open) {
                        self.close();
                    } else {
                        self.open();
                    }
                }
            };
            var io = shmi.visuals.io,
                ml = new io.MouseListener(head_el, funcs),
                tl = new io.TouchListener(head_el, funcs);

            self.listeners.push(ml, tl);
        } else {
            if (!lbl_el) {
                shmi.notify("[ProjectWindow] template missing 'label' ui-element (" + self.config.template + ")", "Template Error");
            }
            if (!head_el) {
                shmi.notify("[ProjectWindow] template missing 'header' ui-element (" + self.config.template + ")", "Template Error");
            } else {
                shmi.notify("[ProjectWindow] template missing ui-element (" + self.config.template + ")", "Template Error");
            }
        }

        if (self.config['initial-state'] === 'open') {
            self.open();
        } else {
            shmi.addClass(self.element, 'minimized');
        }
    },
    open: function() {
        if (!this._open) {
            this._open = true;
            shmi.removeClass(this.element, 'minimized');
        }
    },
    close: function() {
        if (this._open) {
            this._open = false;
            shmi.addClass(this.element, 'minimized');
        }
    },
    /**
     * Enables the ProjectWindow
     *
     */
    onEnable: function() {
        this.listeners.forEach(function(l) {
            l.enable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.enable();
        });
        shmi.log("[ProjectWindow] enabled", 1);
    },
    /**
     * Disables the ProjectWindow
     *
     */
    onDisable: function() {
        this.listeners.forEach(function(l) {
            l.disable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.disable();
        });
        shmi.log("[ProjectWindow] disabled", 1);
    },
    /**
     * Locks the ProjectWindow
     *
     */
    onLock: function() {
        this.listeners.forEach(function(l) {
            l.disable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.lock();
        });
        this.locked = true;
        shmi.addClass(this.element, 'locked');
        shmi.log("[ProjectWindow] locked", 1);
    },
    /**
     * Unlocks the ProjectWindow
     *
     */
    onUnlock: function() {
        this.listeners.forEach(function(l) {
            l.enable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.unlock();
        });
        this.locked = false;
        shmi.removeClass(this.element, 'locked');
        shmi.log("[ProjectWindow] locked", 1);
    },
    /**
     * Sets value to the ProjectWindow
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        shmi.log("[ProjectWindow] value set: " + value, 0);
    },
    setLabel: function(labelText) {
        var self = this,
            labelElement = shmi.getUiElement("label", self.element);

        if (labelElement) {
            labelElement.textContent = shmi.localize(labelText);
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }
        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.contentElement, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });
        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive()) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.ProjectWindow, shmi.visuals.core.BaseControl);

/**
 * WebIQ visuals recipe-controller control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "recipe-controller",
 *     "name": null,
 *     "template": "default/recipe-controller"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "RecipeController", // control name in camel-case
        uiType = "recipe-controller", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": className,
        "name": null,
        "template": "default/recipe-controller",
        "recipe-select": null,
        "recipe-edit": null,
        "button-order": [
            "add",
            "clone",
            "remove",
            "edit",
            "capture",
            "apply"
        ],
        "button-usage": {
            "add": true,
            "clone": true,
            "remove": true,
            "edit": true,
            "capture": true,
            "apply": true
        }
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    /**
     * watchRecipeSelection - watch & react to selection of recipes
     *
     * @param  {object} self control instance reference
     * @param  {object} recipeSelect recipe-select control instance
     * @return {undefined}
     */
    function watchRecipeSelection(self, recipeSelect) {
        let selectedTemplateId = null,
            selectedRecipeId = null;

        if (self.vars.select.changeToken) {
            self.vars.select.changeToken.unlisten();
            self.vars.select.changeToken = null;
        }

        if (recipeSelect && recipeSelect.isActive()) {
            const eventType = (self.vars.selectUiType === "iq-recipe-list") ? "select" : "change";
            self.vars.select.changeToken = recipeSelect.listen(eventType, function(evt) {
                console.log("recipe selection changed:", evt);
                updateSelection(self, evt.source.getValue());
                updateButtonState(self);
            });
            self.vars.select.control = recipeSelect;
            if (self.vars.edit.control && self.vars.edit.control.isActive()) {
                selectedTemplateId = recipeSelect.getTemplate();
                if (self.vars.select.selection) selectedRecipeId = self.vars.select.selection.recipe_id;
                if (typeof selectedTemplateId === "number") {
                    self.vars.edit.control.setTemplate(selectedTemplateId, selectedRecipeId, function() {
                        console.log(uiType, "template-ID set");
                    });
                }
            }
            updateSelection(self, recipeSelect.getValue());
        } else {
            self.vars.select.control = null;
            if (self.vars.select.changeToken) {
                self.vars.select.changeToken.unlisten();
                self.vars.select.changeToken = null;
            }
            updateSelection(self, null);
        }
        updateButtonState(self);
    }

    /**
     * updateSelection - update selection data
     *
     * @param  {object} self control instance refenrece
     * @param  {object} selectionValue selection data
     * @return {undefined}
     */
    function updateSelection(self, selectionValue) {
        const editControl = self.vars.edit.control;

        if (selectionValue && selectionValue.recipe_id !== null) {
            self.vars.select.selection = selectionValue;
            if (editControl) {
                editControl.editRecipe({
                    mode: editControl.MODE.EDIT,
                    recipe_id: selectionValue.recipe_id,
                    preview: true
                });
            }
        } else {
            self.vars.select.selection = null;
            if (editControl) {
                editControl.reset();
            }
        }
    }

    /**
     * watchRecipeEdit - watch & connect to recipe-edit instance when available
     *
     * @param  {object} self control instance reference
     * @param  {object} recipeEdit recipe-edit control instance
     * @return {undefined}
     */
    function watchRecipeEdit(self, recipeEdit) {
        let selectedTemplateId = null,
            selectedRecipeId = null;

        if (recipeEdit && recipeEdit.isActive()) {
            self.vars.edit.control = recipeEdit;
            if (self.vars.select.control) {
                selectedTemplateId = self.vars.select.control.getTemplate();
                if (self.vars.select.selection) selectedRecipeId = self.vars.select.selection.recipe_id;
                if (typeof selectedTemplateId === "number") {
                    recipeEdit.setTemplate(selectedTemplateId, selectedRecipeId, function() {
                        console.log(uiType, "template-ID set");
                    });
                }
            }
            self.vars.edit.tokens.push(recipeEdit.listen("start-edit", function() {
                self.vars.isEditing = true;
                updateState(self);
            }));
            self.vars.edit.tokens.push(recipeEdit.listen("stop-edit", function() {
                self.vars.isEditing = false;
                updateState(self);
                if (typeof (self.vars.select.control?.refreshGrid) === "function") {
                    self.vars.select.control.refreshGrid();
                }
            }));
        } else {
            self.vars.edit.control = null;
            self.vars.edit.tokens.forEach(function(t) {
                t.unlisten();
            });
            self.vars.edit.tokens = [];
        }
    }

    /**
     * lockButton - lock button when available
     *
     * @param  {object} self control instance reference
     * @param  {string} name button property name
     * @return {undefined}
     */
    function lockButton(self, name) {
        const buttons = self.vars.buttons;
        if (buttons[name]) {
            buttons[name].lock();
        }
    }

    /**
     * unlockButton - unlock button when available
     *
     * @param  {object} self control instance reference
     * @param  {string} name button property name
     * @return {undefined}
     */
    function unlockButton(self, name) {
        const buttons = self.vars.buttons;
        if (buttons[name]) {
            buttons[name].unlock();
        }
    }

    /**
     * updateButtonState - update state of buttons in controller bar
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function updateButtonState(self) {
        const iter = shmi.requires("visuals.tools.iterate").iterateObject,
            selectControl = self.vars.select.control,
            editControl = self.vars.edit.control,
            selection = self.vars.select.selection,
            isEditing = self.vars.isEditing,
            operationInFlight = self.vars.operationInFlight;

        if (self.locked) {
            return;
        }

        if (isEditing || operationInFlight) {
            iter(self.vars.buttons, (val) => val?.lock());
        } else {
            if (selectControl && selection) {
                if (selection.values_set) {
                    unlockButton(self, "apply");
                } else {
                    lockButton(self, "apply");
                }
                unlockButton(self, "remove");
                unlockButton(self, "capture");
            } else {
                lockButton(self, "apply");
                lockButton(self, "remove");
                lockButton(self, "capture");
            }

            if (editControl) {
                if (selection) {
                    unlockButton(self, "edit");
                    unlockButton(self, "clone");
                } else {
                    lockButton(self, "edit");
                    lockButton(self, "clone");
                }
                unlockButton(self, "add");
            } else {
                lockButton(self, "add");
                lockButton(self, "edit");
                lockButton(self, "clone");
            }
        }
    }

    /**
     * updateState - update state of buttons and attached widgets
     *
     * @param  {object} self control instance reference
     * @param  {boolean} stopResetEdit do not reset the edit control after updating the state
     * @return {undefined}
     */
    function updateState(self, stopResetEdit) {
        const {
            vars: {
                select: {
                    control: controlSelect
                },
                edit: {
                    control: controlEdit
                },
                operationInFlight
            }
        } = self;
        let { vars: { isEditing } } = self;

        // If a widget is missing, cancel editing state.
        if (!controlSelect || !controlEdit) {
            self.vars.isEditing = isEditing = false;
        }

        // Update lock states of the buttons.
        updateButtonState(self);

        // Update lock states of the attached select and edit widgets.
        if (isEditing || operationInFlight) {
            if (controlSelect && !self.vars.select.lockToken) {
                self.vars.select.lockToken = controlSelect.lockRecursive();
            }
        } else {
            if (self.vars.select.lockToken) {
                self.vars.select.lockToken.unlock();
                self.vars.select.lockToken = null;
            }
            if (stopResetEdit) {
                return;
            }
            shmi.decouple(() => controlEdit?.reset());
        }
    }

    /**
     * @param {string} message
     * @returns {Promise<boolean>}
     */
    function confirmAsync(message) {
        return new Promise((resolve) => shmi.confirm(message, (confirmed) => resolve(!!confirmed)));
    }

    /**
     * doCapture - perform actual capture of item values to recipe
     *
     * @param {object} recipe recipe object
     */
    function doCapture(recipe) {
        return new Promise((resolve, reject) => recipe.capture(null, (captureErr) => {
            if (captureErr) {
                reject(new shmi.LocalizedError("${recipe-controller.error.captureRecipe}", {
                    ERROR_CODE: captureErr.errc,
                    ERROR_MSG: captureErr.message,
                    ERROR_CAT: captureErr.category
                }));
            }

            resolve();
        }));
    }

    /**
     * captureRecipe - capture item values to existing recipe, ask for
     * confirmation when existing item values are overwritten
     *
     * @param {object} recipe recipe object
     */
    async function captureRecipe(recipe) {
        let confirmed = true;

        if (typeof recipe.versionId === "number") {
            // Recipe already has a version. Ask if it should be overwritten.
            confirmed = await confirmAsync("${recipe-controller.confirm.captureRecipe}");
        }

        if (!confirmed) {
            return;
        }

        await doCapture(recipe);
    }

    /**
     * fetchRecipe - fetch recipe data
     *
     * @param {number} recipeId recipe ID
     */
    function fetchRecipe(recipeId) {
        return new Promise((resolve, reject) => {
            const rm = shmi.requires("visuals.session.RecipeManager");

            rm.getRecipe(recipeId, (recipe, err) => {
                if (err) {
                    reject(new shmi.LocalizedError("${recipe-controller.error.fetchRecipe}", {
                        ERROR_CODE: err.errc,
                        ERROR_MSG: err.message,
                        ERROR_CAT: err.category
                    }));
                } else {
                    resolve(recipe);
                }
            });
        });
    }

    /**
     * createRecipe - create new recipe with data from editor
     *
     * @param {number} templateId recipe template ID
     * @param {string} recipeName name of recipe
     */
    async function createRecipe(templateId, recipeName) {
        const rm = shmi.requires("visuals.session.RecipeManager");

        const template = await new Promise((resolve, reject) => rm.getTemplate(templateId, (response, err) => {
            if (err) {
                reject(new shmi.LocalizedError("${recipe-controller.error.fetchTemplate}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                }));
            } else {
                resolve(response);
            }
        }));

        return new Promise((resolve, reject) => template.createRecipe(recipeName, {}, (response, err) => {
            if (err) {
                reject(new shmi.LocalizedError("${recipe-controller.error.createRecipe}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                }));
            } else {
                resolve(response);
            }
        }));
    }

    /**
     * cloneRecipe - clone recipe to new recipe
     *
     * @param {number} recipeId recipe ID
     * @param {string} recipeName name of cloned recipe
     */
    async function cloneRecipe(recipeId, recipeName) {
        const rm = shmi.requires("visuals.session.RecipeManager");

        const recipe = await new Promise((resolve, reject) => rm.getRecipe(recipeId, (response, err) => {
            if (err) {
                reject(new shmi.LocalizedError("${recipe-controller.error.fetchRecipe}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                }));
            } else {
                resolve(response);
            }
        }));

        return new Promise((resolve, reject) => recipe.clone(recipeName, (response, err) => {
            if (err) {
                reject(new shmi.LocalizedError("${recipe-controller.error.cloneRecipe}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                }));
            } else {
                resolve(response);
            }
        }));
    }

    /**
     * attachButtonListeners - create & attach click handlers for button bar
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function attachButtonListeners(self) {
        const buttons = self.vars.buttons,
            tokens = self.vars.tokens;

        //add new recipe
        if (buttons.add) {
            tokens.push(buttons.add.listen("click", async function(evt) {
                const editModule = shmi.requires("recipe-controller.ls.recipe-name"),
                    selectControl = self.vars.select.control;

                let templateId = null;

                if (!selectControl) {
                    return;
                }

                templateId = selectControl.getTemplate();
                if (typeof templateId !== "number") {
                    return;
                }

                self.vars.operationInFlight = true;
                updateState(self);

                try {
                    const recipeName = await new Promise((resolve) => editModule.getRecipeName(templateId, (name) => resolve(name)));
                    if (typeof recipeName === "string") {
                        const recipe = await createRecipe(templateId, recipeName);
                        await doCapture(recipe);

                        selectControl.refreshGrid();
                        selectControl.setValue(recipeName);

                        self.fire("change", {
                            type: "add",
                            recipe
                        });
                    }
                } catch (e) {
                    shmi.notify(e.message, "${V_ERROR}");
                }

                self.vars.operationInFlight = false;
                updateState(self);
            }));
        }

        //edit selected recipe
        if (buttons.edit) {
            tokens.push(buttons.edit.listen("click", function(evt) {
                const selection = self.vars.select.selection,
                    control = self.vars.edit.control;

                if (control && selection) {
                    control.editRecipe({
                        mode: control.MODE.EDIT,
                        recipe_id: selection.recipe_id
                    });
                    // event 'stop-edit' will be fired in recipe-edit (invoked in enable() function of this control)
                }
            }));
        }

        //apply selected recipe
        if (buttons.apply) {
            tokens.push(buttons.apply.listen("click", async function(evt) {
                const selection = self.vars.select.selection;

                if (!selection) {
                    return;
                }

                self.vars.operationInFlight = true;
                updateState(self);

                try {
                    const recipe = await fetchRecipe(selection.recipe_id);
                    if (typeof recipe.versionId !== "number") {
                        shmi.notify("${recipe-controller.error.noRecipeValues}", "${V_ERROR}");
                    } else if (await confirmAsync("${recipe-controller.confirm.applyRecipe}")) {
                        await new Promise((resolve, reject) => recipe.apply((response, err) => {
                            if (err) {
                                reject(new shmi.LocalizedError("${recipe-controller.error.applyRecipe}", {
                                    ERROR_CODE: err.errc,
                                    ERROR_MSG: err.message,
                                    ERROR_CAT: err.category
                                }));
                            } else {
                                resolve(response);
                            }
                        }));

                        self.fire("change", {
                            type: "apply",
                            recipe
                        });
                    }
                } catch (e) {
                    shmi.notify(e.message, "${V_ERROR}");
                }

                self.vars.operationInFlight = false;
                updateState(self);
            }));
        }

        //capture recipe values
        if (buttons.capture) {
            tokens.push(buttons.capture.listen("click", async function(evt) {
                const selection = self.vars.select.selection,
                    selectControl = self.vars.select.control;

                if (!selection) {
                    return;
                }

                self.vars.operationInFlight = true;
                updateState(self);

                //capture to existing recipe
                try {
                    const recipe = await fetchRecipe(selection.recipe_id);
                    await captureRecipe(recipe);

                    self.fire("change", {
                        type: "capture",
                        recipe
                    });
                } catch (e) {
                    shmi.notify(e.message, "${V_ERROR}");
                }

                self.vars.operationInFlight = false;
                if (selectControl) updateSelection(self, self.vars.select.selection);
                // update the state after operation finished but do not reset the editor again
                updateState(self, true);
            }));
        }

        //clone recipe
        if (buttons.clone) {
            tokens.push(buttons.clone.listen("click", async function(evt) {
                const editModule = shmi.requires("recipe-controller.ls.recipe-name"),
                    selectControl = self.vars.select.control,
                    selection = self.vars.select.selection;

                if (!selectControl) {
                    return;
                }

                const templateId = selectControl.getTemplate(),
                    recipeName = await new Promise((resolve) => editModule.getRecipeName(templateId, (name) => resolve(name)));

                if (!recipeName) {
                    return;
                }

                self.vars.operationInFlight = true;
                updateState(self);

                try {
                    const recipe = await cloneRecipe(selection.recipe_id, recipeName);

                    if (selectControl.uiType === "iq-recipe-list") {
                        selectControl.refreshGrid();
                        selectControl.setValue(recipe.id);
                    } else {
                        selectControl.setValue(recipeName);
                    }

                    self.fire("change", {
                        type: "apply",
                        templateId
                    });
                } catch (e) {
                    shmi.notify(e.message, "${V_ERROR}");
                    if (typeof (selectControl.refreshGrid) === "function") {
                        selectControl.refreshGrid();
                    }
                }

                self.vars.operationInFlight = false;
                updateState(self);
            }));
        }

        //delete recipe
        if (buttons.remove) {
            tokens.push(buttons.remove.listen("click", async function(evt) {
                const selection = self.vars.select.selection,
                    selectControl = self.vars.select.control,
                    editControl = self.vars.edit.control;

                if (!selection) {
                    return;
                } else if (!await confirmAsync("${recipe-controller.confirm.deleteRecipe}")) {
                    return;
                }

                self.vars.operationInFlight = true;
                updateState(self);

                try {
                    const recipe = await fetchRecipe(selection.recipe_id);
                    await new Promise((resolve, reject) => recipe.delete((err) => {
                        if (err) {
                            reject(new shmi.LocalizedError("${recipe-controller.error.deleteRecipe}", {
                                ERROR_CODE: err.errc,
                                ERROR_MSG: err.message,
                                ERROR_CAT: err.category
                            }));
                        } else {
                            resolve();
                        }
                    }));

                    if (editControl) {
                        const editState = editControl.getState();
                        if (editState && editState.recipe_id === selection.recipe_id) {
                            editControl.reset();
                        }
                    }

                    if (selectControl) {
                        selectControl.setValue("");
                    }

                    if (typeof (selectControl.refreshGrid) === "function") {
                        selectControl.refreshGrid();
                    }

                    self.fire("change", {
                        type: "del",
                        recipe
                    });
                } catch (e) {
                    shmi.notify(e.message, "${V_ERROR}");
                }

                self.vars.operationInFlight = false;
                updateState(self);
            }));
        }
    }

    /**
     * Replaces occurrences of uiType to `iq-button` and maps even iq-variants
     * to `iq-variant-02` and uneven iq-variants to `iq-variant-01`.
     *
     * @param {string} str
     * @param {object} self control instance refenrece
     * @returns {string}
     */
    function toIqButtonVariant(str, self) {
        let mapped = str.replace(uiType, "iq-button");

        // Map iq-user-controller variants to iq-button variants.
        const variantMatches = mapped.match(/iq-variant-(\d+)/);
        if (variantMatches) {
            const variantId = parseInt(variantMatches[1]);
            mapped = mapped.replace(variantMatches[0], `iq-variant-0${1 + (1 + variantId) % 2}`);
        } else {
            mapped = mapped.concat(' iq-variant-01');
        }

        if (self.element.classList.contains('secondary')) {
            mapped = mapped.concat(' secondary');
        }

        if (self.element.classList.contains('tertiary')) {
            mapped = mapped.concat(' tertiary');
        }

        return mapped;
    }

    /**
     * createControllerButton - create button controls to perform actions on recipes
     *
     * @param {object} self control instance refenrece
     * @param {string} name control name, also used as appendix for CSS selector
     * @param {string} label localizable label & hover text
     *
     * @returns {object} button control reference
     */
    function createControllerButton(self, name, label, iconSource, identifier) {
        const buttonConfig = {
            "class-name": `${toIqButtonVariant(self.config["class-name"], self)} ${name}`,
            "label": null,
            "icon-title": null,
            "icon-src": iconSource,
            "icon-class": "icon",
            "show-icon": true,
            "show-text": false,
            "inline-svg-icon": true
        };
        let button = null;

        if (self.config["button-usage"] && self.config["button-usage"][identifier] !== true) {
            return null;
        }

        buttonConfig.label = label;
        buttonConfig["tooltip"] = label;
        buttonConfig["class-name"] += " " + name;
        buttonConfig.name = name;

        button = shmi.createControl("iq-button", self.vars.domContainer || self.element, buttonConfig, "DIV");

        return button;
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, "");
        }
        return configured === instance.getName();
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            buttons: {
                add: null,
                edit: null,
                remove: null,
                apply: null,
                capture: null,
                clone: null
            },
            select: {
                control: null,
                changeToken: null,
                lockToken: null,
                selection: null
            },
            edit: {
                control: null,
                tokens: []
            },
            isEditing: false,
            operationInFlight: false,
            tokens: [],
            eventProxy: null,
            selectUiType: "recipe-select"
        },
        /* imports added at runtime */
        imports: {},
        /* array of custom event types fired by this control */
        events: ["change"],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    buttons = self.vars.buttons,
                    buttonParameters = {
                        add: {
                            name: "add-recipe",
                            label: "${recipe-controller.label.add}",
                            icon: "pics/system/controls/recipe-controller/add-dark.svg"
                        },
                        edit: {
                            name: "edit-recipe",
                            label: "${recipe-controller.label.edit}",
                            icon: "pics/system/controls/recipe-controller/edit-dark.svg"
                        },
                        remove: {
                            name: "remove-recipe",
                            label: "${recipe-controller.label.remove}",
                            icon: "pics/system/controls/recipe-controller/remove-dark.svg"
                        },
                        clone: {
                            name: "clone-recipe",
                            label: "${recipe-controller.label.clone}",
                            icon: "pics/system/controls/recipe-controller/clone-dark.svg"
                        },
                        capture: {
                            name: "capture-recipe",
                            label: "${recipe-controller.label.capture}",
                            icon: "pics/system/controls/recipe-controller/capture-dark.svg"
                        },
                        apply: {
                            name: "apply-recipe",
                            label: "${recipe-controller.label.apply}",
                            icon: "pics/system/controls/recipe-controller/apply-dark.svg"
                        }
                    };

                this.vars.domContainer = shmi.getUiElement("button-container", this.element);

                if (Array.isArray(this.config["button-order"])) {
                    this.config["button-order"].forEach((buttonName) => {
                        if (buttons[buttonName] === null && buttonParameters[buttonName]) {
                            const { name, label, icon } = buttonParameters[buttonName];
                            buttons[buttonName] = createControllerButton(this, name, label, icon, buttonName);
                        }
                    });
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                let recipeSelect = null,
                    recipeEdit = null;

                self.controls.forEach(function(c) {
                    c.enable();
                });

                attachButtonListeners(self);

                if (self.config["recipe-select"] !== null) {
                    recipeSelect = self.vars.select.control = shmi.ctrl(self.config["recipe-select"]);
                    if (recipeSelect) self.vars.selectUiType = recipeSelect.uiType;
                    watchRecipeSelection(self, recipeSelect);
                    self.vars.tokens.push(shmi.listen("enable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-select"], evt.source)) {
                            //on enable
                            recipeSelect = self.vars.select.control = evt.source;
                            watchRecipeSelection(self, recipeSelect);
                            updateSelection(self, recipeSelect.getValue());
                            updateState(self);
                        }
                    }, { "source.uiType": self.vars.selectUiType }));

                    self.vars.tokens.push(shmi.listen("disable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-select"], evt.source)) {
                            //on disable
                            recipeSelect = self.vars.select.control = null;
                            watchRecipeSelection(self, recipeSelect);
                            updateSelection(self, null);
                            updateState(self);
                        }
                    }, { "source.uiType": self.vars.selectUiType }));
                }
                if (recipeSelect) updateSelection(self, recipeSelect.getValue());
                updateState(self);
                if (self.config["recipe-edit"] !== null) {
                    recipeEdit = self.vars.edit.control = shmi.ctrl(self.config["recipe-edit"]);
                    watchRecipeEdit(self, recipeEdit);
                    updateState(self);

                    self.vars.tokens.push(shmi.listen("enable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-edit"], evt.source)) {
                            //on enable
                            recipeEdit = self.vars.edit.control = evt.source;
                            watchRecipeEdit(self, recipeEdit);
                            updateState(self);
                        }
                    }, { "source.uiType": "recipe-edit" }));

                    self.vars.tokens.push(shmi.listen("disable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-edit"], evt.source)) {
                            //on disable
                            recipeEdit = self.vars.edit.control = null;
                            watchRecipeEdit(self, recipeEdit);
                            updateState(self);
                        }
                    }, { "source.uiType": "recipe-edit" }));
                    self.vars.tokens.push(shmi.listen("stop-edit", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-edit"], evt.source)) {
                            const selection = self.vars.select.selection;
                            if (selection) {
                                fetchRecipe(selection.recipe_id).then((recipe) => self.fire("change", {
                                    type: "edit",
                                    recipe: recipe,
                                    cancel: evt.detail.cancel
                                })).catch((e) => shmi.notify(e.message, "${V_ERROR}"));
                            }
                        }
                    }, { "source.uiType": "recipe-edit" }));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;
                self.controls.forEach(function(c) {
                    c.disable();
                });
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];
                if (self.vars.select.changeToken) {
                    self.vars.select.changeToken.unlisten();
                    self.vars.select.changeToken = null;
                }

                self.vars.edit.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.edit.tokens = [];
            },
            onDelete: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                self.controls.forEach(function(c) {
                    c.lock();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.controls.forEach(function(c) {
                    c.unlock();
                });
            }
        }
    };

    // --------- LocalScript to implement "add recipe" dialog ---------

    let LOCALSCRIPT_MODULE = "recipe-controller.ls.recipe-name",
        module = shmi.pkg(LOCALSCRIPT_MODULE),
        CALLBACK = null,
        DIALOG = null,
        DELETE_TOKEN = null;

    module.run = function run(self) {
        const parentName = self.parentContainer.getName();

        let tokens = [];

        shmi.onReady({
            controls: {
                apply: parentName + " .apply",
                name: parentName + ".recipeName"
            }
        }, function(ref) {
            const apply = ref.controls.apply,
                name = ref.controls.name;

            if (!self.isActive()) {
                //cancel when script is no longer active
                return;
            }

            name.setValue("");

            tokens.push(apply.listen("click", function() {
                //apply
                const inputName = name.getValue();
                if (typeof inputName === "string" && inputName.trim() !== "") {
                    if (typeof CALLBACK === "function") {
                        CALLBACK(inputName);
                    }
                    CALLBACK = null;
                    if (DIALOG) {
                        DIALOG.hide();
                    }
                }
            }));
        });

        self.onDisable = function() {
            self.run = false;
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
        };
    };

    /**
     * getRecipeName - request user input of recipe name
     *
     * The `callback` function returns the entered recipe name as its only parameter.
     * When input is canceled, `null` is returned.
     *
     * @param  {number} templateId recipe template ID
     * @param  {function} callback function to call on completion
     * @return {undefined}
     */
    module.getRecipeName = function getRecipeName(templateId, callback) {
        let closeTok = null;

        if (typeof CALLBACK === "function") {
            throw new Error("Recipe name dialog still in use!");
        }

        CALLBACK = callback;

        if (DIALOG === null) {
            DIALOG = shmi.createControl("dialog-box", document.body, {
                "title": "${recipe-controller.title.nameDialog}",
                "name": "recipeNameDialog",
                "class-name": "dialog-box recipe-name",
                "content-template": "default/recipe-controller/name-dialog",
                "top-level": true,
                "tab-limit": false
            }, "DIV");

            if (DELETE_TOKEN) {
                DELETE_TOKEN.unlisten();
                DELETE_TOKEN = null;
            }

            DELETE_TOKEN = DIALOG.listen("delete", function(evt) {
                DELETE_TOKEN.unlisten();
                DELETE_TOKEN = null;
                DIALOG = null;
            });
        }

        if (DIALOG) {
            shmi.onActive([DIALOG], function() {
                DIALOG.show();
                closeTok = DIALOG.listen("close", function() {
                    closeTok.unlisten();
                    if (typeof CALLBACK === "function") {
                        CALLBACK(null);
                    }
                    CALLBACK = null;
                });
            });
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "recipe-edit",
 *     "name": null,
 *     "template": "custom/controls/recipe-edit"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "RecipeEdit", // control name in camel-case
        uiType = "recipe-edit", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": "recipe-edit",
        "name": null,
        "template": "default/recipe-edit",
        "treeViewTemplate": "default/recipe-edit/treeview",
        "title": uiType,
        "viewMode": "linear",
        "editName": true,
        "itemFetchTimeout": 10000,
        "expand-icon-src": "pics/system/controls/recipe-edit/arrow_right.svg"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * Available RecipeEdit operation modes
     */
    const MODE = {
            CREATE: 1,
            EDIT: 2,
            CLONE: 3,
            CAPTURE: 4
        },
        applyLabels = [
            null, //first element is null to use `MODE` property values to match button label
            "${recipe-edit.label.create}",
            "${recipe-edit.label.apply}",
            "${recipe-edit.label.clone}",
            "${recipe-edit.label.create}"
        ];

    // declare private functions - START

    /**
     * clearGenerated - clear generated controls from recipe editor content area
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function clearGenerated(self) {
        const tokens = self.vars.tokens;

        // Create a slice of of `self.controls` because `shmi.deleteControl` modifies the controls array.
        self.controls.slice().forEach(function(c) {
            shmi.deleteControl(c, true);
        });

        if (tokens.apply) {
            tokens.apply.unlisten();
            tokens.apply = null;
        }

        if (tokens.cancel) {
            tokens.cancel.unlisten();
            tokens.cancel = null;
        }

        self.vars.controls = {
            name: null,
            items: {},
            meta: {},
            buttons: {}
        };
    }

    /**
     * generateContent - generate recipe editor content area controls for specified mode
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateContent(self) {
        generateName(self);
        generateMeta(self);
        generateItems(self);
        generateButtons(self);
    }

    /**
     * generateName - generate input control(-s) for recipe name
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateName(self) {
        const anchors = self.vars.anchors,
            controls = self.vars.controls;

        controls.name = shmi.createControl("iq-input-field", anchors.name, {
            "label": "${recipe-edit.label.recipeName}",
            "type": shmi.Constants.TYPE_STRING,
            "auto-type": false,
            "class-name": `iq-input-field iq-variant-01 outlined${self.vars.colorVariantClass}`,
            "template": "default/iq-input-field.outlined.iq-variant-01",
            "unit-text": ""
        });
        if (!self.config.editName && controls.name) shmi.addClass(controls.name.element, "hidden");
    }

    /**
     * getMetaType - Description
     *
     * @param {object} self control instance reference
     * @param {string} metaProp meta data property name
     *
     * @returns {number|null} item type of meta property
     */
    function getMetaType(self, metaProp) {
        const recipe = self.vars.recipe,
            c = shmi.Constants,
            metaToType = {
                "string": c.TYPE_STRING,
                "int": c.TYPE_INT,
                "long": c.TYPE_INT,
                "longlong": c.TYPE_INT,
                "uint": c.TYPE_INT,
                "ulong": c.TYPE_INT,
                "ulonglong": c.TYPE_INT,
                "float": c.TYPE_FLOAT,
                "double": c.TYPE_FLOAT,
                "bool": c.TYPE_BOOL,
                "boolean": c.TYPE_BOOL
            };
        let entry = null;

        recipe.meta.some(function(m) {
            if (m.name === metaProp) {
                entry = m;
                return true;
            }
            return false;
        });

        return entry ? metaToType[entry.type] : null;
    }

    /**
     * generateMeta - generate input controls for recipe meta data
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateMeta(self) {
        const anchors = self.vars.anchors,
            controls = self.vars.controls,
            recipe = self.vars.recipe;

        recipe.meta.forEach(function(meta) {
            const type = getMetaType(self, meta.name);
            if (type === shmi.Constants.TYPE_BOOL) {
                controls.meta[meta.name] = shmi.createControl("iq-flip-switch", anchors.meta, {
                    "ui": "iq-flip-switch",
                    "label": meta.label || meta.name,
                    "class-name": `iq-flip-switch iq-variant-01 recipe-bool${self.vars.colorVariantClass}`,
                    "template": "default/iq-flip-switch.iq-variant-01",
                    "on-value": true,
                    "off-value": false
                }, "DIV");
            } else {
                controls.meta[meta.name] = shmi.createControl("iq-input-field", anchors.meta, {
                    "label": meta.label || meta.name,
                    "type": type,
                    "auto-type": false,
                    "unit-text": "",
                    "value-alignment": "auto",
                    "class-name": `iq-input-field iq-variant-01 outlined${self.vars.colorVariantClass}`,
                    "template": "default/iq-input-field.outlined.iq-variant-01"
                }, "DIV");
            }
        });
    }

    /**
    * This function returns a mouse handler for the marker
    *
    * @param  {object} self control instance reference
    * @param  {object} target element
    * @return {object} mouse handler
    */

    function getRowHandler(self, el) {
        return {
            onPress: function(x, y, evt) {
            },
            onEnter: function(evt) {
                if (!self.locked) {
                    shmi.addClass(el.parentNode, "hover");
                }
            },
            onLeave: function(evt) {
                shmi.removeClass(el.parentNode, "hover");
            },
            onRelease: function(x, y, evt) {
                if (!self.locked) {
                    if (evt.path.find((rowPart) => rowPart.getAttribute && (rowPart.getAttribute("data-ui") === "iq-select-box" ||
                            rowPart.getAttribute("data-ui") === "iq-input-field" ||
                            rowPart.getAttribute("data-ui") === "iq-flip-switch"))
                    ) {
                        return;
                    }
                    const arrow = shmi.getUiElement("expand", el);
                    if (shmi.hasClass(arrow, "expand")) {
                        shmi.removeClass(arrow, "expand");
                    } else {
                        shmi.addClass(arrow, "expand");
                    }
                    const anchor = shmi.getUiElement("branch-anchor", el.parentNode);
                    if (anchor.style.display === "none") {
                        anchor.style.display = "flex";
                    } else {
                        anchor.style.display = "none";
                    }
                }
            }
        };
    }

    /**
    * This function returns a touch handler for the marker
    *
    * @param  {object} self control instance reference
    * @param  {object} target element
    * @return {object} touch handler
    */
    function getRowHandlerTouch(self, el) {
        return {
            onPress: function(x, y, evt) {
                self.vars.performClick = true;
            },
            onDrag: function(evt) {
                self.vars.performClick = false;
            },
            onRelease: function(x, y, evt) {
                if (!self.locked && self.vars.performClick) {
                    const arrow = shmi.getUiElement("expand", el);
                    if (shmi.hasClass(arrow, "expand")) {
                        shmi.removeClass(arrow, "expand");
                    } else {
                        shmi.addClass(arrow, "expand");
                    }
                    const anchor = shmi.getUiElement("branch-anchor", el.parentNode);
                    if (anchor.style.display === "none") {
                        anchor.style.display = "flex";
                    } else {
                        anchor.style.display = "none";
                    }
                }
            }
        };
    }

    /**
     * buildTreeView - generate tree view out of items object
     *
     * @param  {object} self control instance reference
     * @param  {object} items object
     * @param  {object} anchor element
     * @return {undefined}
     */

    function buildTreeView(self, itemsObj, anchor) {
        const { iterateObject } = shmi.requires("visuals.tools.iterate"),
            { applyIcon } = shmi.requires("visuals.tools.icons"),
            /** @type {{ treeViewHTMLTemplate: HTMLTemplateElement | null }} */
            { treeViewHTMLTemplate } = self.vars;

        if (!treeViewHTMLTemplate) {
            // Couldn't load tree view template.
            return;
        }

        if (!anchor) {
            anchor = self.vars.anchors.items;
        }

        iterateObject(itemsObj, function(val, el) {
            if (el === "myLabel" || el === "hasChildren") {
                return;
            }

            const newDiv = document.createElement('div');
            newDiv.setAttribute("data-ui", "tree-node");
            newDiv.setAttribute("class", "element cont");
            newDiv.append(treeViewHTMLTemplate.content.cloneNode(true));

            const cName = shmi.getUiElement("control-name", newDiv),
                arrow = shmi.getUiElement("expand", newDiv);

            applyIcon(arrow, self.config["expand-icon-src"], null, true);

            if ((typeof itemsObj[el] === "object") && (itemsObj[el].hasChildren)) {
                const target = shmi.getUiElement("branch-anchor", newDiv),
                    t = shmi.getUiElement("info", newDiv),
                    label = itemsObj[el]["myLabel"];

                shmi.addClass(newDiv, "isNode");
                target.style.display = "none";
                cName.textContent = shmi.localize(label);
                self.vars.rowHandlerTokens.push(new shmi.visuals.io.MouseListener(t, getRowHandler(self, t)));
                self.vars.rowHandlerTokens.push(new shmi.visuals.io.TouchListener(t, getRowHandlerTouch(self, t)));

                if (itemsObj[el]?.actualItem) {
                    const inputAnchor = shmi.getUiElement("value-holder", newDiv);
                    generateItemInput(itemsObj[el].actualItem, inputAnchor, self);
                }
                buildTreeView(self, itemsObj[el], target);
                anchor.appendChild(newDiv);
            } else if (itemsObj[el]?.actualItem) {
                const inputAnchor = shmi.getUiElement("value-holder", newDiv);
                arrow.style.display = "none";

                generateItemInput(itemsObj[el].actualItem, inputAnchor, self);
                anchor.appendChild(newDiv);
            }
        });
    }

    /**
     * generateItemInput - create control for item
     *
     * @param  {object} item object
     * @param  {object} anchor element
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateItemInput(item, anchor, self) {
        const controls = self.vars.controls,
            uc = shmi.requires("shmi.visuals.tools.unitClasses");

        let enableNumpad = false,
            inputOptions = null;

        if (item.read) {
            if (item.type === shmi.Constants.TYPE_BOOL) {
                inputOptions = {
                    "ui": "iq-flip-switch",
                    "label": item.label || item.name,
                    "class-name": `iq-flip-switch iq-variant-01 recipe-bool${self.vars.colorVariantClass}`,
                    "template": "default/iq-flip-switch.iq-variant-01",
                    "on-value": 1,
                    "off-value": 0
                };
            } else if (self.vars.dictionary && self.vars.dictionary["itemoptions-" + item.name]) {
                inputOptions = {
                    "ui": "iq-select-box",
                    "label": item.label || item.name,
                    "class-name": "iq-select-box iq-variant-01 recipe-option"
                };
                inputOptions.options = self.vars.dictionary["itemoptions-" + item.name];
            } else {
                if (self.config.enableNumpad && item.type !== shmi.Constants.TYPE_STRING) enableNumpad = true;
                inputOptions = {
                    "ui": "iq-input-field",
                    "label": item.label || item.name,
                    "class-name": `iq-input-field iq-variant-01 outlined${self.vars.colorVariantClass}`,
                    "template": "default/iq-input-field.outlined.iq-variant-01",
                    "numpad-enabled": enableNumpad,
                    "auto-type": false,
                    "value-alignment": "auto",
                    "unit-text": item.unit || "",
                    "type": item.type,
                    "precision": (typeof item.digits === "number") ? item.digits : -1,
                    "step": (typeof item.step === "number" && item.type === shmi.Constants.TYPE_FLOAT) ? item.step : (item.type === shmi.Constants.TYPE_FLOAT) ? 0 : 1
                };
                if ([shmi.Constants.TYPE_INT, shmi.Constants.TYPE_FLOAT].indexOf(item.type) !== -1) {
                    if (item.adapter) {
                        const selectedAdapter = uc.getSelectedAdapter(item.unitClass);
                        if (typeof item.min === "number") {
                            inputOptions.min = selectedAdapter.outFunction(item.min);
                        }
                        if (typeof item.max === "number") {
                            inputOptions.max = selectedAdapter.outFunction(item.max);
                        }
                    } else {
                        if (typeof item.min === "number") {
                            inputOptions.min = item.min;
                        }
                        if (typeof item.max === "number") {
                            inputOptions.max = item.max;
                        }
                    }
                }
            }

            controls.items[item.name] = shmi.createControl(inputOptions.ui, anchor, inputOptions, "DIV");
            if (!item.write) {
                controls.items[item.name].lock(true);
            }
        }
    }

    /**
     * buildItemsObject - build items object for generating a tree view out of it
     *
     * @param  {object} recipe
     * @return {object} items object
     */

    function buildItemsObject(recipe) {
        const itemsObj = {};
        recipe.items.forEach(function(item) {
            const currItem = {};
            currItem.raw = item.name.split(".");
            currItem.parts = [];
            currItem.index = [];

            currItem.raw.forEach(function(el, idx) { // find all indices
                currItem.index[idx] = [];
                if (el.indexOf("[") !== -1) {
                    const indexes = (el.substring(el.indexOf("[")).split("[").join("").split("]"));
                    indexes.forEach(function(el2, idx2) {
                        currItem.index[idx][idx2] = el2;
                    });
                    currItem.parts[idx] = el.substring(0, el.indexOf("["));
                } else {
                    currItem.parts[idx] = el;
                    currItem.index[idx][0] = "";
                }
            });

            const start = currItem.parts[0] + currItem.index[0][0];
            let root = {};

            if (currItem.parts.length > 1 || currItem.index.length > 1) { // multipart item
                if (!itemsObj[start]) itemsObj[start] = {};
                itemsObj[start].myLabel = "${" + currItem.parts[0] + "} " + currItem.index[0][0];
                root = itemsObj[start];
                root.hasChildren = true;

                for (let k = 1; k < currItem.index[0].length-1; k++) {
                    if (!root[currItem.index[0][k]]) root[currItem.index[0][k]] = {};
                    root = root[currItem.index[0][k]];
                }

                for (let i = 1; i < currItem.parts.length; i++) {
                    const t = currItem.parts[i] + currItem.index[i][0];
                    if (!root[t]) root[t] = {};
                    root[t].myLabel = "${" + currItem.parts[i] + "} " + currItem.index[i][0];
                    if (i === currItem.parts.length-1) {
                        root[t].actualItem = item;
                        root[t].value = "";
                    } else {
                        root = root[t];
                        root.hasChildren = true;
                    }
                }
            } else { // simple item w/o . and index
                if (!itemsObj[start]) {
                    itemsObj[start] = {};
                }
                itemsObj[start].actualItem = item;
                itemsObj[start].value = "";
            }
        });
        return itemsObj;
    }

    /**
     * generateItems - generate input controls for recipe item data
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateItems(self) {
        const anchors = self.vars.anchors,
            recipe = self.vars.recipe;

        if (self.config.viewMode === "linear") {
            recipe.items.forEach(function(item) {
                generateItemInput(item, anchors.items, self);
            });
        }

        if (self.config.viewMode === "tree") {
            const itemsObj = buildItemsObject(recipe),
                anchor = self.vars.anchors.items;
            while (anchor.firstChild) {
                anchor.removeChild(anchor.firstChild);
            }
            buildTreeView(self, itemsObj);
        }

        // Collect new controls and store them in the controls array.
        const collectControls = (fromObject, toArray) => {
            toArray.push(...Object.values(fromObject).filter((ctrl) => !!ctrl));
        };

        if (self.vars.controls.name) {
            self.controls.push(self.vars.controls.name);
        }
        collectControls(self.vars.controls.items, self.controls);
        collectControls(self.vars.controls.meta, self.controls);
        collectControls(self.vars.controls.buttons, self.controls);

        self.vars.rowHandlerTokens.forEach(function(t) {
            t.enable();
        });
    }

    /**
     * getMetaData - get meta data input values
     *
     * @param  {object} self control instance reference
     * @return {object} meta data input
     */
    function getMetaData(self) {
        const iter = shmi.requires("visuals.tools.iterate").iterateObject,
            meta = {};

        iter(self.vars.controls.meta, function(val, prop) {
            meta[prop] = val.getValue();
        });

        return meta;
    }

    /**
     * getItemData - get item data input values
     *
     * @param  {object} self control instance reference
     * @return {object} item data input
     */
    function getItemData(self) {
        const recipe = self.vars.recipe,
            itemData = {},
            uc = shmi.requires("shmi.visuals.tools.unitClasses");

        recipe.items.forEach(function(item, idx) {
            const control = self.vars.controls.items[item.name];
            if (control && item.read && item.write) {
                if (item.adapter) {
                    const selectedAdapter = uc.getSelectedAdapter(item.unitClass);
                    itemData[item.name] = selectedAdapter.inFunction(control.getValue());
                } else {
                    itemData[item.name] = control.getValue();
                }
            }
        });

        return itemData;
    }

    /**
     * createRecipe - create new recipe with data from editor
     *
     * @param  {object} self control instance reference
     * @param  {function} callback function to call on completion
     * @return {undefined}
     */
    function createRecipe(self, callback) {
        const rm = shmi.requires("visuals.session.RecipeManager"),
            controls = self.vars.controls,
            editor = self.vars.editor;

        rm.getTemplate(editor.template_id, function(response, err) {
            if (err) {
                shmi.notify("${recipe-edit.error.fetchTemplate}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                response.createRecipe(controls.name.getValue(), getMetaData(self), function(createResponse, createErr) {
                    if (createErr) {
                        shmi.notify("${recipe-edit.error.createRecipe}", "${V_ERROR}", {
                            ERROR_CODE: createErr.errc,
                            ERROR_MSG: createErr.message,
                            ERROR_CAT: createErr.category
                        });
                        callback(null, createErr);
                    } else {
                        callback(createResponse, null);
                    }
                });
            }
        });
    }

    /**
     * updateRecipe - update existing recipe data
     *
     * @param  {object} self control instance reference
     * @param  {function} callback function called on completion
     * @return {undefined}
     */
    function updateRecipe(self, callback) {
        const rm = shmi.requires("visuals.session.RecipeManager"),
            controls = self.vars.controls,
            editor = self.vars.editor;

        rm.getRecipe(editor.recipe_id, function(response, err) {
            if (err) {
                shmi.notify("${recipe-edit.error.fetchRecipe}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                response.set(controls.name.getValue(), getMetaData(self), false, function(setErr) {
                    if (setErr) {
                        shmi.notify("${recipe-edit.error.updateRecipe}", "${V_ERROR}", {
                            ERROR_CODE: setErr.errc,
                            ERROR_MSG: setErr.message,
                            ERROR_CAT: setErr.category
                        });
                        callback(setErr);
                    } else if (typeof response.versionId === "number") {
                        response.write(getItemData(self), function(writeErr) {
                            if (writeErr) {
                                shmi.notify("${recipe-edit.error.writeRecipe}", "${V_ERROR}", {
                                    ERROR_CODE: writeErr.errc,
                                    ERROR_MSG: writeErr.message,
                                    ERROR_CAT: writeErr.category
                                });
                            }
                            callback(writeErr || null);
                        });
                    } else {
                        callback(null);
                    }
                });
            }
        });
    }

    /**
     * captureRecipe - capture current item values to specified recipe
     *
     * @param  {object} recipe recipe object
     * @param  {function} callback function to call on completion
     * @return {undefined}
     */
    function captureRecipe(recipe, callback) {
        recipe.capture(null, function(captureErr) {
            if (captureErr) {
                shmi.notify("${recipe-edit.error.captureRecipe}", "${V_ERROR}", {
                    ERROR_CODE: captureErr.errc,
                    ERROR_MSG: captureErr.message,
                    ERROR_CAT: captureErr.category
                });
            }
            callback(captureErr || null);
        });
    }

    /**
     * generateButtons - generate apply/capture & cancel buttons for recipe editor
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateButtons(self) {
        const anchors = self.vars.anchors,
            editor = self.vars.editor,
            tokens = self.vars.tokens,
            controls = self.vars.controls;

        if (self.config.viewMode === "tree") {
            controls.buttons.expandAll = shmi.createControl("iq-button", anchors.expandCollapse, {
                "class-name": `iq-button iq-variant-01 outlined${self.vars.colorVariantClass}`,
                "label": null,
                "icon-src": "pics/system/controls/recipe-edit/expand-all.svg",
                "icon-title": "${recipe-edit.label.expand-all}",
                "show-icon": true,
                "show-text": false,
                "name": "expandAll",
                "inline-svg-icon": true
            }, "DIV");
            tokens.expandAll = controls.buttons.expandAll.listen("click", function(evt) {
                const list = self.element.getElementsByClassName("cont");
                for (let item of list) {
                    const anchor = shmi.getUiElement("branch-anchor", item),
                        arrow = shmi.getUiElement("expand", item);

                    anchor.style.display = "flex";
                    shmi.addClass(arrow, "expand");
                }
            });
            controls.buttons.collapseAll = shmi.createControl("iq-button", anchors.expandCollapse, {
                "class-name": `iq-button iq-variant-01 outlined${self.vars.colorVariantClass}`,
                "label": null,
                "icon-src": "pics/system/controls/recipe-edit/collapse-all.svg",
                "icon-title": "${recipe-edit.label.collapse-all}",
                "show-icon": true,
                "show-text": false,
                "name": "collapseAll",
                "inline-svg-icon": true
            }, "DIV");
            tokens.collapseAll = controls.buttons.collapseAll.listen("click", function(evt) {
                const list = self.element.getElementsByClassName("cont");
                for (let item of list) {
                    const anchor = shmi.getUiElement("branch-anchor", item),
                        arrow = shmi.getUiElement("expand", item);

                    anchor.style.display = "none";
                    shmi.removeClass(arrow, "expand");
                }
            });
        }

        controls.buttons.apply = shmi.createControl("iq-button", anchors.buttons, {
            "class-name": `iq-button iq-variant-01 tonal${self.vars.colorVariantClass}`,
            "label": applyLabels[MODE.EDIT],
            "name": "applyRecipe"
        }, "DIV");
        tokens.apply = controls.buttons.apply.listen("click", function(evt) {
            switch (editor.mode) {
            case MODE.CREATE:
                createRecipe(self, function(recipe, err) {
                    if (err) {
                        console.error(uiType, "error creating recipe:", err);
                    } else {
                        console.log(uiType, "created new recipe");
                        clearValues(self);
                        self.onLock();
                        self.fire("stop-edit", { recipe: recipe, cancel: false });
                    }
                });
                break;
            case MODE.EDIT:
                updateRecipe(self, function(err) {
                    if (err) {
                        console.error(uiType, "error updating recipe:", err);
                    } else {
                        console.log(uiType, "recipe updated");
                        clearValues(self);
                        self.onLock();
                        self.fire("stop-edit", { recipe: self, cancel: false });
                    }
                });
                break;
            case MODE.CAPTURE:
                createRecipe(self, function(recipe, err) {
                    if (err) {
                        console.error(uiType, "error creating recipe:", err);
                    } else {
                        console.log(uiType, "created new recipe");
                        captureRecipe(recipe, function(captureErr) {
                            if (captureErr) {
                                console.error(uiType, "error capturing recipe values:", captureErr);
                            } else {
                                console.log(uiType, "recipe values captured");
                                clearValues(self);
                                self.onLock();
                                self.fire("stop-edit", { recipe: recipe, cancel: false });
                            }
                        });
                    }
                });
                break;
            default:
            }
        });
        controls.buttons.cancel = shmi.createControl("iq-button", anchors.buttons, {
            "class-name": `iq-button iq-variant-01 outlined${self.vars.colorVariantClass}`,
            "label": "${recipe-edit.label.cancel}",
            "name": "cancelRecipe"
        }, "DIV");
        tokens.cancel = controls.buttons.cancel.listen("click", function(evt) {
            clearValues(self);
            self.onLock();
            self.fire("stop-edit", { cancel: true });
        });
    }

    /**
     * updateVisibility - update visibility of editor components match specified mode
     *
     * @param {object} self control instance reference
     * @param {number} mode editor mode
     *
     * @returns {undefined}
     */
    function updateVisibility(self, mode) {
        const anchors = self.vars.anchors,
            recipe = self.vars.recipe;

        switch (mode) {
        case MODE.CREATE:
        case MODE.CAPTURE:
            shmi.removeClass(anchors.meta, "hidden");
            shmi.addClass(anchors.items, "hidden");
            break;
        case MODE.EDIT:
            shmi.removeClass(anchors.meta, "hidden");
            if (recipe.valuesSet) {
                shmi.removeClass(anchors.items, "hidden");
            } else {
                shmi.addClass(anchors.items, "hidden");
            }
            break;
        default:
            throw new Error("[RecipeEdit] updateVisibility - Invalid mode specified: " + mode);
        }
    }

    /**
     * clearValues - reset editor values to default state
     *
     * @param {object} self control instance reference
     *
     * @returns {undefined}
     */
    function clearValues(self) {
        const recipe = self.vars.recipe = shmi.cloneObject(self.vars.template),
            editor = self.vars.editor,
            controls = self.vars.controls;

        recipe.valuesSet = false;
        editor.recipe_id = null;
        editor.mode = null;
        controls.name.setValue("");
        recipe.meta.forEach(function(meta) {
            controls.meta[meta.name].setValue("");
        });
        recipe.items.forEach(function(item) {
            if (controls.items[item.name]) {
                controls.items[item.name].setValue(null);
            }
        });
    }

    /**
     * updateValues - update editor component values
     *
     * @param {object} self control instance reference
     * @param {number} mode editor mode
     *
     * @returns {undefined}
     */
    function updateValues(self, mode) {
        const recipe = self.vars.recipe,
            controls = self.vars.controls,
            uc = shmi.requires("shmi.visuals.tools.unitClasses");

        controls.name.setValue(recipe.name);
        controls.buttons.apply.setLabel(applyLabels[mode]);

        switch (mode) {
        case MODE.CREATE:
        case MODE.CAPTURE:
            recipe.meta.forEach(function(meta) {
                controls.meta[meta.name].setValue(meta.default);
            });
            break;
        case MODE.EDIT:
            recipe.meta.forEach(function(meta) {
                controls.meta[meta.name].setValue(meta.value);
            });
            recipe.items.forEach(function(item) {
                if (controls.items[item.name]) {
                    if (item.adapter) {
                        const selectedAdapter = uc.getSelectedAdapter(item.unitClass);
                        controls.items[item.name].setValue(selectedAdapter.outFunction(item.value));
                    } else {
                        controls.items[item.name].setValue(item.value);
                    }
                }
            });
            break;
        default:
            throw new Error("[RecipeEdit] updateValues - Invalid mode specified: " + mode);
        }
    }

    /**
     * fetchTemplateData - fetch template data from connect
     *
     * @param {object} self control instance reference
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function fetchTemplateData(self, callback) {
        const template = self.vars.template,
            editor = self.vars.editor,
            iter = shmi.requires("visuals.tools.iterate").iterateObject,
            rm = shmi.requires("visuals.session.RecipeManager");

        rm.getTemplate(editor.template_id, function(response, err) {
            template.items = [];
            template.meta = [];
            if (err) {
                shmi.notify("${recipe-edit.error.fetchTemplate}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                template.name = response.name;
                response.items.forEach(function(itemName) {
                    template.items.push({
                        name: itemName,
                        label: null,
                        unit: null,
                        type: null,
                        value: null,
                        digits: null,
                        write: false,
                        read: false
                    });
                });
                iter(response.metadata, function(val, prop) {
                    template.meta.push({
                        name: prop,
                        default: val.default_value,
                        label: val.label || prop,
                        type: val.value_type,
                        value: null
                    });
                });
                callback(null);
            }
        });
    }

    /**
     * getItemByName - get item value from stored recipe data
     *
     * @param {object} self control instance reference
     * @param {string} itemName name of item
     *
     * @returns {number|string|boolean|null} item value or `null` if not present
     */
    function getItemByName(self, itemName) {
        const recipe = self.vars.recipe;
        let itemIdx = -1;

        recipe.items.some(function(itm, idx) {
            if (itm.name === itemName) {
                itemIdx = idx;
                return true;
            }
            return false;
        });

        return (itemIdx !== -1) ? recipe.items[itemIdx] : null;
    }

    /**
     * getMetaByName - get meta data value from stored recipe data
     *
     * @param {object} self control instance reference
     * @param {string} metaName name of meta data property
     *
     * @returns {number|string|boolean|null} meta data value or `null` if not present
     */
    function getMetaByName(self, metaName) {
        const recipe = self.vars.recipe;
        let metaIdx = -1;

        recipe.meta.some(function(m, idx) {
            if (m.name === metaName) {
                metaIdx = idx;
                return true;
            }
            return false;
        });

        return (metaIdx !== -1) ? recipe.meta[metaIdx] : null;
    }

    /**
     * fetchRecipeData - fetch recipe data from connect
     *
     * @param {object} self control instance reference
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function fetchRecipeData(self, callback) {
        const editor = self.vars.editor,
            recipe = self.vars.recipe = shmi.cloneObject(self.vars.template),
            iter = shmi.requires("visuals.tools.iterate").iterateObject,
            rm = shmi.requires("visuals.session.RecipeManager");

        rm.getRecipe(editor.recipe_id, function(response, err) {
            if (err) {
                shmi.notify("${recipe-edit.error.fetchRecipe}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                recipe.name = response.name;
                iter(response.metadata, function(val, prop) {
                    const m = getMetaByName(self, prop);

                    if (m) {
                        m.value = val;
                    }
                });
                iter(response.values, function(val, prop) {
                    const itm = getItemByName(self, prop);

                    if (itm) {
                        itm.value = val.value;
                    }
                });
                if (typeof response.versionId === "number") {
                    recipe.valuesSet = true;
                } else {
                    recipe.valuesSet = false;
                }
                callback(null);
            }
        });
    }

    /**
     * get numeric unit-class identifier
     *
     * @param {string} unitText unit text
     * @return {number|null} unit-class identifier or `null`
     */
    function getUnitClass(unitText) {
        if (String(parseInt(unitText)) === unitText) {
            const unitClass = parseInt(unitText);
            return unitClass;
        }

        return null;
    }

    /**
     * get type mapping from string to numerical
     *
     * @param {string} value string type name
     * @return {number} numerical type id
     */
    function getType(value) {
        const typeInfo = {
            "int": 2,
            "long": 2,
            "longlong": 2,
            "uint": 2,
            "ulong": 2,
            "ulonglong": 2,
            "float": 3,
            "double": 3,
            "bool": 1,
            "boolean": 1,
            "string": 0,
            "error": -1
        };

        if (value === null) {
            return null; //after unsubscribe
        } else {
            return typeInfo[value];
        }
    }

    /**
     * fetchItemInfo - fetch item info from item subscriptions
     *
     * @param {object} self control instance refernce
     */
    async function fetchItemInfo(self) {
        const im = shmi.requires("visuals.session.ItemManager"),
            { getSelectedAdapter } = shmi.requires("visuals.tools.unitClasses"),
            itemNames = self.vars.template.items.map((itemInfo) => itemInfo.name),
            itemProperties = await im.getItemProperties(itemNames),
            retrievedItemNames = Object.keys(itemProperties),
            missingItemNames = itemNames.filter((itemName) => !retrievedItemNames.includes(itemName));

        //assign item properties
        self.vars.template.items.forEach((itemInfo) => {
            const properties = itemProperties[itemInfo.name];

            if (properties) {
                const unitClass = getUnitClass(properties.unit),
                    adapter = (unitClass !== null) ? getSelectedAdapter(unitClass) : null,
                    type = getType(properties.value_type);

                Object.assign(itemInfo, {
                    type: getType(properties.value_type),
                    label: properties.label,
                    unit: adapter?.unitText ?? properties.unit,
                    unitClass,
                    adapter,
                    digits: properties.digits,
                    step: properties.stepsize ?? 0,
                    min: properties.min,
                    max: properties.max,
                    read: properties.access.includes("read") && (type !== -1),
                    write: properties.access.includes("write"),
                    error: (type !== -1)
                });
            }
        });

        if (missingItemNames.length !== 0) {
            const err = new Error(`Unable to load item info for ${missingItemNames.length} item(s).`);
            err.items = missingItemNames;

            console.error(err, err.items);
            throw err;
        }
    }

    /**
     * initAnchorElements - initialize required template anchor elements
     *
     * @param {object} self control instance reference
     *
     * @returns {undefined}
     */
    function initAnchorElements(self) {
        const iter = shmi.requires("visuals.tools.iterate").iterateObject,
            anchors = self.vars.anchors;

        anchors.body = shmi.getUiElement("editor-body", self.element);
        anchors.name = shmi.getUiElement("name-anchor", self.element);
        anchors.meta = shmi.getUiElement("meta-anchor", self.element);
        anchors.items = shmi.getUiElement("items-anchor", self.element);
        anchors.expandCollapse = shmi.getUiElement("expand-collapse-anchor", self.element);
        anchors.buttons = shmi.getUiElement("buttons-anchor", self.element);

        iter(anchors, function(val, prop) {
            if (val === null) {
                throw new Error("[RecipeEdit] template does not contain all required anchor elements");
            }
        });
    }

    /**
     * updateEditor - prepeare editor area for use
     *
     * @param {object} self control instance reference
     * @oaram {boolean} preview make editor read-only for preview
     * @return {undefined}
     */
    function updateEditor(self, preview) {
        const editor = self.vars.editor;

        if (preview === true) {
            self.onLock();
        } else {
            self.onUnlock();
        }

        updateVisibility(self, editor.mode);
        updateValues(self, editor.mode);
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            anchors: {
                body: null,
                name: null,
                meta: null,
                items: null,
                buttons: null
            },
            controls: {
                name: null,
                items: {},
                meta: {},
                buttons: {}
            },
            tokens: {
                apply: null,
                cancel: null
            },
            editor: {
                template_id: null,
                recipe_id: null,
                mode: null
            },
            recipe: {
                name: null,
                meta: [],
                items: []
            },
            template: {
                name: null,
                meta: [],
                items: []
            },
            /** @type {HTMLTemplateElement | null} */
            treeViewHTMLTemplate: null,
            rowHandlerTokens: [],
            templateSet: false,
            performClick: true,
            fetchId: null,
            dictionary: null,
            expandIcon: null,
            colorVariantClass: ""
        },
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: ["start-edit", "stop-edit"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            onTemplate: async function(baseWidgetHTML, failed, nextCallback) {
                this.vars = this.vars || {};

                if (failed) {
                    console.error(`[${className}] Template file '${shmi.c("TEMPLATE_PATH") + this.config.template + shmi.c("TEMPLATE_EXT")}' not found  - 404`);
                    nextCallback();
                    return;
                }

                // Load the treeview template. Just continue if it fails.
                try {
                    let treeTemplateResult = null;

                    if (this.config.viewMode !== "tree") {
                        // Not in tree view mode, don't load tree view template.
                    } else if (!this.config.treeViewTemplate) {
                        console.error(`[${className}] No treeview template specified.`);
                    } else if (this.config.treeViewTemplate.startsWith(shmi.c("RES_URL_PREFIX"))) {
                        treeTemplateResult = await shmi.loadResourcePromise(this.config.treeViewTemplate);
                    } else if (this.config.treeViewTemplate.startsWith(":")) {
                        treeTemplateResult = await shmi.loadResourcePromise(this.config.treeViewTemplate.replace(":", "") + shmi.c("TEMPLATE_EXT"));
                    } else {
                        treeTemplateResult = await shmi.loadResourcePromise(shmi.c("TEMPLATE_PATH") + this.config.treeViewTemplate + shmi.c("TEMPLATE_EXT"));
                    }

                    if (treeTemplateResult) {
                        const treeTemplate = document.createElement("template");

                        treeTemplate.innerHTML = treeTemplateResult;
                        this.vars.treeViewHTMLTemplate = treeTemplate;
                        shmi.addClass(this.element, "tree-view");
                    }
                } catch (e) {
                    console.error(`[${className}] Failed to load treeview template:`, e);
                }

                if (this.element instanceof HTMLElement && baseWidgetHTML) {
                    this.element.innerHTML = baseWidgetHTML;
                }

                nextCallback();
            },
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                initAnchorElements(self);

                if (self.config.dictionary) {
                    self.vars.dictionary = shmi.requires(self.config.dictionary).dictionary;
                }
                if (shmi.hasClass(self.element, "secondary")) {
                    self.vars.colorVariantClass = " secondary";
                } else if (shmi.hasClass(self.element, "tertiary")) {
                    self.vars.colorVariantClass = " tertiary";
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;
                self.vars.rowHandlerTokens.forEach(function(t) {
                    t.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;
                self.vars.rowHandlerTokens.forEach(function(t) {
                    t.disable();
                });
                self.vars.rowHandlerTokens = [];
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                self.controls.forEach(function(c) {
                    if (self.vars.editor.preview && (c.config.name === "collapseAll" || c.config.name === "expandAll")) {
                        c.unlock();
                    } else {
                        c.lock();
                    }
                });
                if (self.vars.editor.preview) {
                    self.vars.rowHandlerTokens.forEach(function(listener) {
                        listener.enable();
                        shmi.removeClass(listener.element, "locked");
                    });
                } else {
                    self.vars.rowHandlerTokens.forEach(function(listener) {
                        listener.disable();
                        shmi.addClass(listener.element, "locked");
                    });
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.controls.forEach(function(c) {
                    c.unlock();
                });
                self.vars.rowHandlerTokens.forEach(function(listener) {
                    listener.enable();
                    shmi.removeClass(listener.element, "locked");
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            },
            setTemplate: function(templateId, recipeId, callback) {
                const self = this,
                    editor = self.vars.editor;

                let fetchId = null;

                self.vars.fetchId = {};

                if (self.isActive()) {
                    editor.template_id = templateId;
                    self.vars.templateSet = false;
                    fetchId = self.vars.fetchId; //guard object to only update editor content on last request sent
                    fetchTemplateData(self, async function(err) {
                        if (!err) {
                            if (self.isActive()) {
                                try {
                                    await fetchItemInfo(self);

                                    if (self.vars.fetchId === fetchId) {
                                        self.vars.fetchId = null;
                                        self.vars.templateSet = true;
                                        self.vars.recipe = shmi.cloneObject(self.vars.template);
                                        if (self.isActive()) {
                                            clearGenerated(self);
                                            generateContent(self);
                                            if (recipeId !== null) {
                                                self.editRecipe({
                                                    mode: MODE.EDIT,
                                                    recipe_id: recipeId,
                                                    preview: true
                                                });
                                            } else {
                                                self.onLock();
                                            }
                                        }
                                    }
                                } catch (err2) {
                                    if (!self.isActive()) {
                                        // Widget is inactive - don't do anything
                                    } else if (err2.items) {
                                        clearGenerated(self);
                                        shmi.notify("${recipe-edit.error.fetchItemInfoTimeout}", "${V_ERROR}", {
                                            ITEM_COUNT: err2.items.length,
                                            // Dot out items if there's more than 3
                                            ITEMS: (err2.items.length > 3 ? [...err2.items.slice(0, 3), "..."] : err2.items).join(", ")
                                        });
                                    } else {
                                        clearGenerated(self);
                                        shmi.notify("${recipe-edit.error.fetchItemInfo}", "${V_ERROR}");
                                    }
                                }
                            }
                            callback(null);
                        } else {
                            callback(err);
                        }
                    });
                } else {
                    throw new Error("[" + uiType + "] control not enabled yet!");
                }
            },
            isTemplateSet: function() {
                return this.vars.templateSet;
            },
            /**
             * editRecipe - open recipe for editing in recipe-edit
             *
             * @param {object} options recipe edit options
             * @param {number} options.mode recipe edit mode
             * @param {number} options.recipe_id recipe ID
             * @param {boolean} [options.preview] read-only when set to `true`
             * @return {undefined}
             */
            editRecipe: function(options) {
                const self = this,
                    editor = self.vars.editor;

                if (self.isActive() && self.isTemplateSet()) {
                    editor.mode = options.mode;
                    editor.recipe_id = options.recipe_id;
                    editor.preview = (options.preview === true);
                    if (editor.recipe_id !== null) {
                        if (!editor.preview) {
                            self.fire("start-edit", {});
                        }
                        fetchRecipeData(self, function(err) {
                            if (!err) {
                                updateEditor(self, editor.preview);
                            }
                        });
                    } else {
                        self.vars.recipe = {
                            name: null,
                            meta: [],
                            items: []
                        };
                        updateEditor(self, editor.preview);
                    }
                }
            },
            reset: function() {
                const self = this,
                    editor = self.vars.editor,
                    currentTemplateId = editor.template_id;

                if (self.isActive() && self.isTemplateSet()) {
                    editor.template_id = null;
                    editor.recipe_id = null;
                    editor.mode = null;
                    editor.preview = null;
                    self.setTemplate(currentTemplateId, null, function(err) {
                        if (err) {
                            console.error(uiType, "error resetting recipe-edit:", err);
                        }
                    });
                }
            },
            getState: function() {
                return shmi.cloneObject(this.vars.editor);
            },
            MODE: MODE
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals recipe-select control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "recipe-select",
 *     "name": null,
 *     "template": "default/recipe-select",
 *     "show-results": "two-or-more",
 *     "recipe-template-id": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "recipe-select", // control name in camel-case
        uiType = "recipe-select", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    var defConfig = {
        "class-name": className,
        "name": null,
        "template": "default/recipe-select",
        "label": uiType,
        "show-results": "always",
        "recipe-template-id": null,
        "default-value": null,
        "clear-on-focus": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function eventProxy(self, event) {
        self.fire(event.type, event.detail);
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domChild: null,
            dataGridId: null,
            dataGrid: null,
            searchbar: null,
            eventProxy: null,
            tokens: []
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                if (typeof (this.config["recipe-template-id"]) !== "number") {
                    this.element.textContent = "Invalid or no recipe template id given. This control will not work. Please select a valid recipe template id.";
                    fLog("Invalid or no recipe template id given:", this.config["recipe-template-id"]);
                    return;
                }

                this.vars.domChild = shmi.getUiElement("recipe-select-searchbar", this.element);
                if (this.vars.domChild === null) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                this.imports.dgm.grids[this.vars.dataGridId] = this.vars.dataGrid = new shmi.visuals.core.DataGridRecipe(this.vars.dataGridId, this.config["recipe-template-id"]);
                this.vars.dataGrid.init();

                var searchbarConfig = {
                    "class-name": "iq-searchbar iq-variant-01",
                    "label": this.config["label"],
                    "datagrid": this.vars.dataGridId,
                    "search-column": "recipe_name",
                    "show-results": this.config["show-results"],
                    "result-columns": ["recipe_name"],
                    "partial-word-search": true
                };

                this.vars.searchbar = shmi.createControl("iq-searchbar", this.vars.domChild, searchbarConfig);
                if (this.vars.searchbar === null) {
                    this.element.textContent = "Unable to create searchbar control for recipes. This control will not work.";
                    fLog("Unable to create searchbar control for recipes.");
                }
                const appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`
                if (this.config['clear-on-focus']) {
                    this.vars.tokens.push(this.vars.searchbar.listen("enter", () => {
                        if (!keyboardEnabled) {
                            this.vars.searchbar.setValue("");
                        }
                    }));
                }
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {object} selected recipe info
             */
            getValue: function() {
                var matchData = null,
                    recipeId = null,
                    valuesSet = null,
                    nameValue = null;

                if (this.vars.searchbar) {
                    matchData = this.vars.searchbar.getMatchData();
                    if (matchData !== null) {
                        recipeId = parseInt(matchData.recipe_id);
                        valuesSet = parseInt(matchData.values_set);
                        nameValue = this.vars.searchbar.getValue();
                        if (nameValue === matchData.recipe_name) {
                            return {
                                name: this.vars.searchbar.getValue(),
                                recipe_id: isNaN(recipeId) ? null : recipeId,
                                template_id: this.config["recipe-template-id"],
                                values_set: (valuesSet === 1)
                            };
                        }
                    }
                }

                return null;
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                if (this.vars.searchbar) {
                    this.vars.searchbar.setValue(val);
                }
            },
            refreshGrid: function() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.vars.searchbar) {
                    this.vars.eventProxy = this.vars.searchbar.listen("change", eventProxy.bind(null, this));
                    this.vars.searchbar.enable();
                    if (this.config["default-value"] !== null) {
                        this.vars.searchbar.setValue(this.config["default-value"]);
                    }
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.disable();
                }

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }
            },
            onDelete: function() {
                if (this.vars.searchbar) {
                    shmi.deleteControl(this.vars.searchbar);
                }

                if (this.vars.dataGrid) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
                this.vars.tokens.forEach((tok) => {
                    tok.unlisten();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.lock();
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.unlock();
                }
            },
            getTemplate: function() {
                return this.config["recipe-template-id"];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");

/**
 * Creates a new Screen control
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.Screen = function(element, config) {
    /* check for required packages */
    shmi.requires("visuals.controls.View");

    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'screen');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'initial-view', 0);
    shmi.def(this.config, 'views', []);
    shmi.def(this.config, 'no-views-content', '<h4> -- add views to this screen -- </h4>');
    shmi.def(this.config, 'delete-inactive-views', true);

    this.controls = [];
    this.viewElements = [];
    this.currentView = -1;
    this.listeners = [];
    this.subscriptionId = null;
    this.tokens = [];

    this.startup();
};

shmi.visuals.controls.Screen.prototype = {
    uiType: "screen",
    isContainer: true,
    events: ['navigate'],
    getClassName: function() {
        return "Screen";
    },
    onRegister: function(onDone, templateData) {
        const self = this;

        if (Array.isArray(self.config.views) && self.config.views.length) {
            if (typeof templateData === "string") {
                self.element.innerHTML = templateData;
            } else {
                console.warn("[" + self.getClassName() + "]", "Screen option 'views' is deprecated and should no longer be used. Use a single content template specified with the 'template' option instead.");
                shmi.multiLoad(self.config.views, (results) => {
                    if (self.isDeleted()) {
                        return;
                    }
                    const viewData = results.map((r) => {
                        if (!r.failed) {
                            return r.data + "\n";
                        }
                        return "";
                    }).join("");

                    self.onRegister(onDone, viewData);
                });
                return;
            }
        }

        const tmp_els = shmi.getUiElements('view', this.element);
        for (let i = 0; i < tmp_els.length; i++) {
            if (tmp_els[i].parentNode === this.element) {
                this.viewElements.push(tmp_els[i]);
            }
        }

        /* remove all child nodes */
        this.clear();

        if (!this.config['delete-inactive-views']) {
            this.viewElements.forEach((elem, idx) => {
                const vElem = elem.cloneNode(true);

                shmi.addClass(vElem, "hidden");
                self.element.appendChild(vElem);
                vElem.setAttribute("data-index", idx);
            });
            self.parseChildren(self.element, onDone);
        } else {
            onDone();
        }
    },
    onInit: function() {
        var self = this;
        if (self.viewElements.length > self.config['initial-view']) {
            self.setView(self.config['initial-view']);
        }
    },
    clear: function() {
        while (this.element.childNodes.length > 0) {
            this.element.removeChild(this.element.childNodes[0]);
        }
    },
    onSetValue: function(value) {
        if (shmi.isDesignerEditor()) {
            return;
        }
        var idx = parseInt(value);
        this.setView(idx);
    },
    onLock: function() {
        var self = this;

        self.controls.forEach((ctrl) => ctrl.lock());
        shmi.log("[Screen] onLock called", 2);
    },
    onUnlock: function() {
        var self = this;

        self.controls.forEach((ctrl) => ctrl.unlock());
        shmi.log("[Screen] onUnlock called", 2);
    },
    onEnable: function() {
        if (this.currentView !== -1) {
            this.activeView.enable();
        }
        var IM = shmi.visuals.session.ItemManager;
        if (this.config.item) {
            this._sID = IM.subscribeItem(this.config.item, this);
            console.log("screen subscribed " + this.config.item);
        }
    },
    onDisable: function() {
        var self = this,
            IM = shmi.visuals.session.ItemManager;

        if (this.config.item) {
            IM.unsubscribeItem(this.config.item, this._sID);
            console.log("screen unsubscribed item");
        }

        if (this.currentView !== -1) {
            this.activeView.disable();
        }

        self.tokens.forEach(function(t) {
            t.unlisten();
        });
    },
    setView: function(index) {
        var self = this;
        if (this.currentView === index) {
            shmi.log("[Screen] view " + index + " already active", 2);
        } else if (!this.viewElements[index]) {
            console.error("[" + this.getClassName() + "] invalid view index:", index);
        } else {
            var old_index = this.currentView;
            if (old_index !== -1 && this.activeView) {
                if (this.config['delete-inactive-views']) {
                    shmi.deleteControl(this.activeView, true);
                } else {
                    shmi.addClass(this.activeView.element, "hidden");
                    this.activeView.disable();
                }
            }

            var newelem;
            if (this.config['delete-inactive-views']) {
                newelem = this.viewElements[index].cloneNode(true);
                this.element.appendChild(newelem);
                const nodeId = newelem.getAttribute("_nodeid");
                let viewConfig = {};
                if (nodeId) {
                    viewConfig = shmi.visuals.model.getConfig(nodeId);
                }
                viewConfig.index = index;
                this.activeView = shmi.createControl('view', newelem, viewConfig, 'DIV', 'from');
            } else {
                this.activeView = this.controls[index];
                shmi.removeClass(this.activeView.element, "hidden");
            }

            if (this.active) {
                this.activeView.enable();
            }

            this.currentView = index;
            var IM = shmi.visuals.session.ItemManager;
            if (this.config.item) {
                if (IM.readValue(this.config.item) !== this.currentView) {
                    IM.writeValue(this.config.item, this.currentView);
                }
            }

            var e_detail = { index: index, old_index: old_index };
            if (this.activeView.isActive()) {
                this.fire('navigate', e_detail);
            } else {
                var tokActiveView = this.activeView.listen("enable", function() {
                    var idx = -1;
                    tokActiveView.unlisten();
                    idx = self.tokens.indexOf(tokActiveView);
                    if (idx !== -1) {
                        self.tokens.splice(idx, 1);
                    }
                    tokActiveView = null;
                    self.fire('navigate', e_detail);
                });
                self.tokens.push(tokActiveView);
            }
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            notView = false,
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (self.config["delete-inactive-views"]) {
            callback(new Error("Screen is configured to delete inactive views. Disable option 'delete-inactive-views' to programatically add child controls."), controls);
            return;
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        notView = options.some(function(option) {
            return (option.ui !== "view");
        });

        if (notView) {
            callback(new Error("All children must be of type 'view'"), controls);
            return;
        }

        options.forEach(function(option) {
            var control = null;

            option.config.index = self.controls.length;
            control = shmi.createControl(option.ui, self.element, option.config, "DIV", null, false);

            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                shmi.addClass(control.element, "hidden");
                self.viewElements.push(control.element.cloneNode(true));
                controls.push(control);
            }
        });

        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }

                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    if (self.controls.length && self.controls.length === controls.length) {
                        self.setView(self.config["initial-view"]);
                    }
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                if (self.controls.length && self.controls.length === controls.length) {
                    self.setView(self.config["initial-view"]);
                }
                callback(errors.length ? errors[0] : null, controls);
            }
        });

        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.Screen, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");

shmi.visuals.controls.SlideIn = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'slide-in');
    shmi.def(this.config, 'class-name-container', 'iq-container');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'template', 'default/slide-in');
    shmi.def(this.config, 'always-open-layouts', []);
    shmi.def(this.config, 'container-layouts', []);
    shmi.def(this.config, 'auto-layout', true);
    shmi.def(this.config, 'initial-mode', 'slide');
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'auto-close', false);
    shmi.def(this.config, 'tab-limit', true);
    shmi.def(this.config, 'overlay-style', {
        background: "transparent",
        zIndex: 1
    });

    this.controls = [];
    this.state = {};
    this.state.open = false;
    this.forceOpen = false;
    this.containerMode = false;

    this.startup();
};

shmi.visuals.controls.SlideIn.prototype = {
    uiType: "slide-in",
    isContainer: true,
    events: ["open", "close"],
    getClassName: function() {
        return "SlideIn";
    },
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }

        var cur_layout = shmi.getCurrentLayout();
        if (this.config['always-open-layouts'].indexOf(cur_layout) !== -1) {
            this.forceOpen = true;
        } else {
            this.forceOpen = false;
        }

        if (this.config['initial-mode'] === "container") {
            this.enableContainerLayout();
        }

        if (this.config['auto-layout'] === true) {
            this.testContainerLayout();
        }

        if (this.forceOpen) {
            this.open();
        }
    },
    onDisable: function() {
        var self = this;
        this.close();
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].disable();
        }
        if (Array.isArray(self.openListeners)) {
            self.openListeners.forEach(function(l) {
                l.disable();
            });
            self.openListeners = [];
        }
    },
    onDelete: function() {
        if ((this.config['container-layouts'].length > 0) && (this.config['auto-layout'] === true)) {
            window.removeEventListener('visuals-layout-change', this.testContainerLayoutBind);
        }

        if ((this.config['always-open-layouts'].length > 0)) {
            window.removeEventListener('visuals-layout-change', this.testOpenLayoutBind);
        }
    },
    open: function() {
        var self = this;
        if (!this.state.open) {
            if (this.containerMode === false) {
                shmi.removeClass(this.element, 'closed');
                shmi.addClass(this.element, 'open');
            }

            if (self.config['tab-limit']) {
                // Enable element tabulator
                var tabulator = shmi.requires("visuals.tools.tabulator");
                self.tabLimit = tabulator.setTabParent(self.element);
            }

            if (self.config['auto-close']) {
                var openHandler = {
                    onPress: function(x, y, evt) {
                        if (!shmi.testParentChild(self.element, evt.target)) {
                            evt.preventDefault();
                            self.close();
                            self.openListeners.forEach(function(l) {
                                l.disable();
                            });
                            self.openListeners = [];
                        }
                    }
                };
                this.bgOverlay = document.createElement("DIV");
                this.bgOverlay.classList.add("overlay-background");

                this.bgOverlay.style.background = self.config["overlay-style"].background;
                this.bgOverlay.style.zIndex = self.config["overlay-style"].zIndex;
                this.bgOverlay.style.top = 0;
                this.bgOverlay.style.right = 0;
                this.bgOverlay.style.bottom = 0;
                this.bgOverlay.style.left = 0;
                this.bgOverlay.style.position = "fixed";

                document.body.insertBefore(this.bgOverlay, document.body.firstChild);
                this.openListeners = [
                    new shmi.visuals.io.MouseListener(this.bgOverlay, openHandler),
                    new shmi.visuals.io.TouchListener(this.bgOverlay, openHandler)
                ];
                this.openListeners.forEach(function(l) {
                    l.enable();
                });
            }

            this.state.open = true;
            this.fire("open", {});
        }
    },
    close: function() {
        var self = this;
        if (this.state.open) {
            if (this.containerMode === false) {
                shmi.removeClass(this.element, 'open');
                shmi.addClass(this.element, 'closed');
            }

            if (self.tabLimit) {
                self.tabLimit.unsetTabParent();
                self.tabLimit = null;
            }

            if (self.config['auto-close']) {
                if (Array.isArray(self.openListeners)) {
                    self.openListeners.forEach(function(l) {
                        l.disable();
                    });
                    self.openListeners = [];
                }
                if (this.bgOverlay) {
                    this.bgOverlay.parentNode.removeChild(this.bgOverlay);
                    this.bgOverlay = null;
                }
            }
            this.state.open = false;
            this.fire("close", {});
        }
    },
    toggle: function() {
        if (this.state.open) {
            this.close();
        } else {
            this.open();
        }
    },
    onRegister: function(onDone) {
        var self = this;

        self.contentElement = shmi.getUiElement("slide-in-content", self.element);

        if (self.contentElement && self.config["content-template"]) {
            var templateUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                self.config["content-template"] :
                shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");

            shmi.loadResource(templateUrl, function(data, failed) {
                if (self.isDeleted()) {
                    return;
                }

                if (!failed) {
                    self.contentElement.innerHTML = data;
                    self.parseChildren(self.contentElement, onDone);
                } else {
                    onDone();
                }
            });
        } else if (self.contentElement) {
            self.parseChildren(self.contentElement, onDone);
        } else {
            onDone();
        }
    },
    onInit: function() {
        shmi.removeClass(this.element, 'open');
        shmi.addClass(this.element, 'closed');
        this.state.open = false;

        if (this.config['always-open-layouts'].length > 0) {
            this.testOpenLayoutBind = this.testOpenLayout.bind(this);
            window.addEventListener('visuals-layout-change', this.testOpenLayoutBind, false);
        }

        if (this.config['container-layouts'].length > 0) {
            if (this.config['auto-layout'] === true) {
                this.testContainerLayoutBind = this.testContainerLayout.bind(this);
                window.addEventListener('visuals-layout-change', this.testContainerLayoutBind, false);
            }
        }
    },
    testOpenLayout: function() {
        var cur_layout = shmi.getCurrentLayout();
        if (this.config['always-open-layouts'].indexOf(cur_layout) !== -1) {
            this.forceOpen = true;
            if (this.active) {
                this.open();
            }
        } else {
            this.forceOpen = false;
            if (this.active) {
                this.close();
            }
        }
    },
    enableContainerLayout: function() {
        if (this.containerMode === false) {
            this.containerMode = true;
            shmi.removeClass(this.element, this.config['class-name']);
            shmi.removeClass(this.element, 'open');
            shmi.removeClass(this.element, 'closed');
            shmi.addClass(this.element, this.config['class-name-container']);
            if (this.active) {
                this.open();
            }
        }
    },
    disableContainerLayout: function() {
        if (this.containerMode === true) {
            this.containerMode = false;
            shmi.removeClass(this.element, this.config['class-name-container']);
            shmi.addClass(this.element, this.config['class-name']);
            if (this.state.open) {
                shmi.addClass(this.element, 'open');
            } else {
                shmi.addClass(this.element, 'closed');
            }
            if (this.active) {
                this.close();
            }
        }
    },
    toggleContainerLayout: function() {
        if (this.containerMode === false) {
            this.containerMode = true;
            shmi.removeClass(this.element, this.config['class-name']);
            shmi.removeClass(this.element, 'open');
            shmi.removeClass(this.element, 'closed');
            shmi.addClass(this.element, this.config['class-name-container']);
            if (this.active) {
                this.open();
            }
        } else if (this.containerMode === true) {
            this.containerMode = false;
            shmi.removeClass(this.element, this.config['class-name-container']);
            shmi.addClass(this.element, this.config['class-name']);
            if (this.state.open) {
                shmi.addClass(this.element, 'open');
            } else {
                shmi.addClass(this.element, 'closed');
            }
            if (this.active) {
                this.close();
            }
        }
    },
    testContainerLayout: function() {
        var cur_layout = shmi.getCurrentLayout();
        if (this.config['container-layouts'].indexOf(cur_layout) !== -1) {
            if (this.containerMode === false) {
                this.containerMode = true;
                shmi.removeClass(this.element, this.config['class-name']);
                shmi.removeClass(this.element, 'open');
                shmi.removeClass(this.element, 'closed');
                shmi.addClass(this.element, this.config['class-name-container']);
                if (this.active) {
                    this.open();
                }
            }
        } else if (this.containerMode === true) {
            this.containerMode = false;
            shmi.removeClass(this.element, this.config['class-name-container']);
            shmi.addClass(this.element, this.config['class-name']);
            if (this.state.open) {
                shmi.addClass(this.element, 'open');
            } else {
                shmi.addClass(this.element, 'closed');
            }
            if (this.active) {
                this.close();
            }
        }
    },
    onLock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].lock();
        }
    },
    onUnlock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].unlock();
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }
        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.contentElement, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });
        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive()) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.SlideIn, shmi.visuals.core.BaseControl);

/** Exported UI-Actions **/
shmi.pkg("visuals.session");
shmi.visuals.session.userActions = shmi.visuals.session.userActions || {};

shmi.visuals.session.userActions['slide-in'] = function(params) {
    var owner = null; //arguments[4];
    if (params.length !== 2) {
        shmi.log("[UserAction] slide-in - needs two parameters", 2);
    } else {
        var slide_in = shmi.ctrl(params[1], owner);
        switch (params[0]) {
        case 'open':
            if (slide_in) {
                slide_in.open();
            } else {
                shmi.log("[UserAction] slide-in - control '" + params[1] + "' not found", 2);
            }
            break;
        case 'close':
            if (slide_in) {
                slide_in.close();
            } else {
                shmi.log("[UserAction] slide-in - control '" + params[1] + "' not found", 2);
            }
            break;
        case 'toggle':
            if (slide_in) {
                slide_in.toggle();
            } else {
                shmi.log("[UserAction] slide-in - control '" + params[1] + "' not found", 2);
            }
            break;
        default:
            shmi.log("[UserAction] unknown function '" + params[0] + "' configured", 3);
        }
    }
};

shmi.pkg("visuals.controls");
/**
 * TabPanel to execute UI actions
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - the base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.TabPanel = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    /*set default options */
    shmi.def(this.config, 'class-name', 'tab-panel');
    shmi.def(this.config, 'template', 'default/tab-panel');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'initial-view', 0);
    shmi.def(this.config, 'tab-labels', []);
    shmi.def(this.config, 'views', []);
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'delete-inactive-views', false);
    shmi.def(this.config, 'item', null);

    this.mouselistener = null;
    this.touchlistener = null;
    this.tabScrollHandler = null;
    this.active = false;
    this.initialized = false;
    this._subscriptionTargetId = null;

    this.buttons = [];
    this.screen = null;
    this.controls = [];

    this.startup();
};

shmi.visuals.controls.TabPanel.prototype = {
    uiType: "tab-panel",
    events: ['navigate'],
    isContainer: true,
    getClassName: function() {
        return "TabPanel";
    },
    onRegister: function(onDone) {
        const self = this;
        let initToken = null;

        self.screen_anchor = shmi.getUiElement('screen-anchor', self.element);
        if (!self.screen_anchor) {
            console.warn("[" + self.getClassName() + "]", "'screen-anchor' element missing from template, adding replacement");
            const screenAnchor = document.createElement("DIV");
            screenAnchor.setAttribute("class", "iqel-tab-content");
            self.element.appendChild(screenAnchor);
            self.screen_anchor = screenAnchor;
        }

        self.screen = shmi.createControl('screen', self.screen_anchor, {
            "name": "content",
            "no-views-content": "",
            "views": self.config.views,
            "template": self.config["content-template"],
            "initial-view": self.config['initial-view'],
            "delete-inactive-views": self.config['delete-inactive-views'],
            "item": self.config.item
        }, 'DIV');

        initToken = shmi.waitOnInit([self.screen], function() {
            if (initToken) {
                const idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken = null;
            }
            onDone();
        });

        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    },
    /**
     * Initializes the control.
     *
     * @param responseText - content of the loaded template
     */
    onInit: function() {
        const self = this,
            l = self.config["tab-labels"];

        self.tabs_anchor = shmi.getUiElement('tabs-anchor', self.element);
        if (!self.tabs_anchor) {
            shmi.notify("${V_MISSING_UI_ELEMENT} 'tabs-anchor'", "${V_ERROR}");
        } else {
            self.screen.viewElements.forEach(function(ve, i) {
                self.buttons.push(
                    shmi.createControl("iq-button", self.tabs_anchor, {
                        "label": l[i] || "view_" + (i + 1),
                        "class-name": "iqel-tab-button"
                    }, 'DIV')
                );
                if (i === self.config['initial-view']) {
                    shmi.addClass(self.buttons[i].element, "selected");
                }
                self.buttons[i].listen('click', self.setView.bind(self, i));
                if (shmi.hasClass(self.element, 'small')) {
                    shmi.addClass(self.buttons[i].element, 'small');
                } else if (shmi.hasClass(self.element, 'large')) {
                    shmi.addClass(self.buttons[i].element, 'large');
                }
            });
        }

        self.tabScrollHandler = (evt) => {
            if (self.tabs_anchor && !evt.shiftKey && evt.deltaY) {
                self.tabs_anchor.scrollLeft += evt.deltaY;
                evt.preventDefault();
            }
        };
    },
    setView: function(index) {
        const self = this;
        if (!self.initialized || !self.screen.initialized) {
            return;
        }
        if (self.screen && self.buttons[index]) {
            self.screen.setView(index);
            for (let i = 0; i < self.buttons.length; i++) {
                shmi.removeClass(self.buttons[i].element, 'selected');
            }
            shmi.addClass(self.buttons[index].element, 'selected');
            self.buttons[index].element.scrollIntoView({
                behavior: "smooth",
                inline: "center",
                block: "nearest"
            });
        }
    },
    onEnable: function() {
        const self = this,
            im = shmi.requires("visuals.session.ItemManager"),
            h = im.getItemHandler();

        self.controls.forEach(function(c) {
            c.enable();
        });

        if (self.config.item) {
            h.setValue = function setValue(v) {
                if (shmi.isDesignerEditor()) {
                    return;
                }
                const idx = parseInt(v);
                if (!isNaN(idx) && (idx >= 0) && (idx < self.buttons.length)) {
                    self.buttons.forEach(function(b) {
                        shmi.removeClass(b.element, "selected");
                    });
                    shmi.addClass(self.buttons[idx].element, "selected");
                }
            };
            self._subscriptionTargetId = im.subscribeItem(self.config.item, h);
        }

        if (self.tabs_anchor) {
            self.tabs_anchor.addEventListener("wheel", self.tabScrollHandler);
        }
    },
    onDisable: function() {
        const self = this;

        self.controls.forEach(function(c) {
            c.disable();
        });

        if (self._subscriptionTargetId) {
            self._subscriptionTargetId.unlisten();
            self._subscriptionTargetId = null;
        }

        if (self.tabs_anchor) {
            self.tabs_anchor.removeEventListener("wheel", self.tabScrollHandler);
        }
    },
    onSetValue: function() {

    },
    onLock: function() {
        for (let i = 0; i < this.controls.length; i++) {
            this.controls[i].lock();
        }
        shmi.addClass(this.element, 'locked');
        shmi.log("[TabPanel] locked", 1);
    },
    onUnlock: function() {
        for (let i = 0; i < this.controls.length; i++) {
            this.controls[i].unlock();
        }
        shmi.removeClass(this.element, 'locked');
        shmi.log("[TabPanel] unlocked", 1);
    },
    onAddControl: function(options, callback) {
        const self = this,
            controls = [];
        let notView = false,
            offset = 0;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        notView = options.some(function(option) {
            return (option.ui !== "view");
        });

        if (notView) {
            callback(new Error("All children must be of type 'view'"), controls);
            return;
        }

        offset = self.screen.viewElements.length;

        options.forEach(function(option, idx) {
            const i = offset + idx,
                label = option.config.label || self.config["tab-labels"][i] || "View " + (i + 1);

            self.buttons.push(
                shmi.createControl("iq-button", self.tabs_anchor, {
                    "class-name": "iqel-tab-button",
                    "label": label
                }, 'DIV')
            );
            if (i === self.config['initial-view']) {
                shmi.addClass(self.buttons[i].element, "selected");
            }
            self.buttons[i].listen('click', self.setView.bind(self, i));
            if (shmi.hasClass(self.element, 'small')) {
                shmi.addClass(self.buttons[i].element, 'small');
            } else if (shmi.hasClass(self.element, 'large')) {
                shmi.addClass(self.buttons[i].element, 'large');
            }
        });

        self.screen.addControl(options, callback);
    }
};

shmi.extend(shmi.visuals.controls.TabPanel, shmi.visuals.core.BaseControl);
shmi.registerControlType("tab-panel", shmi.visuals.controls.TabPanel, true);

/**
 * utility classes of Panel control
 *
 * @namespace shmi.visuals.controls.panel
 */
shmi.pkg("visuals.controls.panel");

/**
 * Creates a Panel View element
 *
 * @constructor
 * @param element - base element of the panel view
 */
shmi.visuals.controls.View = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'class-name', 'view inner-layout');
    shmi.def(this.config, 'index', 0);

    if (typeof this.config.index === "string") {
        this.config.index = parseInt(this.config.index);
    }

    this.parentContainer = null;
    this.controls = [];
    this.active = false;
    this.initialized = false;

    this.startup();
};

shmi.visuals.controls.View.prototype = {
    isContainer: true,
    uiType: "view",
    getClassName: function() {
        return "View";
    },
    onRegister: function(onDone) {
        var self = this;

        self.parseChildren(self.element, onDone);
    },
    onInit: function() {
        var self = this;
        self.index = self.config.index;
        shmi.removeClass(self.element, 'ctrl');
    },
    /**
     * Enables the View and child controls
     *
     */
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }
    },
    /**
     * Disables the View and child controls
     *
     */
    onDisable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].disable();
        }
    },
    onLock: function() {
        var self = this;
        self.controls.forEach(function(ctrl) {
            ctrl.lock();
        });
    },
    onUnlock: function() {
        var self = this;
        self.controls.forEach(function(ctrl) {
            ctrl.unlock();
        });
    },
    getIndex: function() {
        return this.index;
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.element, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });

        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }

                if (self.isActive()) {
                    control.enable();
                }

                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.View, shmi.visuals.core.BaseControl);

/* This module contains animation helpers used by iq-linear-gauge and iq-radial-gauge */
(function() {
    const MODULE_NAME = "iqGauge.animationHelper",
        /** @lends module:iqGauge.animationHelper */
        module = shmi.pkg(MODULE_NAME);

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Animates a value from a previous value to the new value within a given
     * timespan.
     *
     * This class is intended to be used with animation frames. Animation
     * frames must call "tick" with their timestamp before accessing the
     * animated value.
     */
    module.AnimationHelper = class AnimationHelper {
        constructor() {
            this.value = null;
            this.targetValue = null;
            this.timestamp = null;
            this.step = null;
        }

        /**
         * Sets the new target value. The animated value will move towards the
         * new target within the given duration.
         *
         * @param {?number} value The target value the animated value is to
         *  move towards.
         * @param {number} [duration] Timespan in milliseconds in which the
         *  animated value will move towards the target value in. If a zero- or
         *  negative duration is given, the animated value will be set to the
         *  target value with the next "tick".
         */
        set(value, duration = 250) {
            if (typeof value !== "number" || this.value === null) {
                // Can't animate non-numbers.
            } else if (duration > 0) {
                this.step = (value - this.value) / duration;
            } else {
                this.step = (value - this.value);
            }
            this.targetValue = value;
        }

        /**
         * Updates the animated value.
         *
         * @param {number} timestamp Current timestamp or timestamp of the
         *  animation frame calling this function.
         * @returns {boolean} `true`, if the animated value has changed,
         *  `false` else.
         */
        tick(timestamp) {
            if (this.value === this.targetValue) {
                this.timestamp = timestamp;
                return false;
            } else if (this.timestamp === null || this.step === null || this.step === 0) {
                this.value = this.targetValue;
            } else {
                const valueDiff = this.step * (timestamp - this.timestamp);
                if (Math.abs(this.value - this.targetValue) < Math.abs(valueDiff)) {
                    this.value = this.targetValue;
                    this.step = null;
                } else {
                    this.value = this.value + valueDiff;
                }
            }

            this.timestamp = timestamp;
            return true;
        }
    };
})();

/* This module contains helpers used by iq-linear-gauge and iq-radial-gauge */
(function() {
    const MODULE_NAME = "iqGauge.gaugeHelpers",
        /** @lends module:iqGauge.gaugeHelpers */
        module = shmi.pkg(MODULE_NAME);

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Bounding box of an arc.
     *
     * @typedef {object} ArcBoundingBox
     *
     * @property {number} top
     * @property {number} left
     * @property {number} bottom
     * @property {number} right
     * @property {number} centerX X coordinate of the arc's origin.
     * @property {number} centerY Y coordinate of the arc's origin.
     * @property {number} width Width of the arc.
     * @property {number} height Height of the arc.
     * @property {number} radius Radius of the arc.
     */

    /**
     * Returns min if value is lower than min, max if the value is larger than
     * max, or value otherwise.
     *
     * @param {number} min Lower bound of the interval
     * @param {number} value Value to clamp.
     * @param {number} max Upper bound of the interval
     * @returns {number} min if value is lower than min, max if the value is
     *  larger than max, or value otherwise.
     */
    function clamp(min, value, max) {
        return Math.max(min, Math.min(value, max));
    }

    /**
     * Computes the normalized bounding box around a rotated gauge with the
     * specified slice angle.
     *
     * @param {number} arcStart Starting angle of the gauge
     * @param {number} arcEnd Ending angle of the gauge.
     * @returns {ArcBoundingBox}
     */
    function computeNormalizedGaugeBoundingBox(arcStart, arcEnd) {
        // Set of points to examine:
        // - End-Points of the partial circle.
        // - Intersections with axis
        const points = [
            { x: Math.cos(arcStart), y: Math.sin(arcStart), theta: arcStart },
            { x: Math.cos(arcEnd), y: Math.sin(arcEnd), theta: arcEnd }
        ];

        // Compute intersetctions with axis
        for (let i = Math.ceil(2 * arcStart / Math.PI); i <= Math.floor(2 * arcEnd / Math.PI); ++i) {
            const theta = i * Math.PI / 2;
            points.push({ x: Math.cos(theta), y: Math.sin(theta), theta });
        }

        // Compute bounding box of points
        return points.reduce((boundingBox, point) => ({
            top: Math.min(boundingBox.top, point.y),
            left: Math.min(boundingBox.left, point.x),
            bottom: Math.max(boundingBox.bottom, point.y),
            right: Math.max(boundingBox.right, point.x),
            centerX: boundingBox.centerX,
            centerY: boundingBox.centerY,
            width: boundingBox.width,
            height: boundingBox.height
        }), {
            top: 1,
            left: 1,
            bottom: -1,
            right: -1,
            centerX: 0,
            centerY: 0,
            width: 2,
            height: 2
        });
    }

    /**
     * Takes a normalized gauge bounding box and translates and scales it such
     * that it will fit exactly within the specified dimensions and is
     * centered.
     *
     * @param {ArcBoundingBox} boundingBox Normalized gauge bounding box computed by
     * @param {number} width Width to fit the gauge bounding box in.
     * @param {number} height Height to fit the gauge bounding box in.
     * @returns {ArcBoundingBox}
     */
    function translateGaugeBoundingBox(boundingBox, width, height) {
        // Compute the size of the longest side of the bounding box.
        const longestSide = (() => {
            const scalerHeight = (boundingBox.bottom - boundingBox.top) / boundingBox.height,
                scalerWidth = (boundingBox.right - boundingBox.left) / boundingBox.width,
                ratioVertical = scalerWidth / scalerHeight,
                ratioHorizontal = scalerHeight / scalerWidth;

            if (boundingBox.right - boundingBox.left > boundingBox.bottom - boundingBox.top) {
                return Math.min(ratioVertical * height, width);
            }

            return Math.min(ratioHorizontal * width, height);
        })();

        // Transformations done on the bounding box:
        // 1. Move upper left corner of bounding box to (0, 0)
        // 2. Scale bounding box such that max(width, height) = 1 while keeping
        //    the aspect ratio.
        // 3. Scale bounding box to fit the target width/height.

        const computeSize = (X, offset, scaler) => (X - offset) / Math.max(boundingBox.right - boundingBox.left, boundingBox.bottom - boundingBox.top) * scaler;

        // Compute dimensions of the translated bounding box.
        const translatedWidth = computeSize(boundingBox.right, boundingBox.left, longestSide),
            translatedHeight = computeSize(boundingBox.bottom, boundingBox.top, longestSide);

        return {
            top: (height - translatedHeight) / 2,
            left: (width - translatedWidth) / 2,
            bottom: (height + translatedHeight) / 2,
            right: (width + translatedWidth) / 2,
            centerY: computeSize(boundingBox.centerY, boundingBox.top, longestSide) + (height - translatedHeight) / 2,
            centerX: computeSize(boundingBox.centerX, boundingBox.left, longestSide) + (width - translatedWidth) / 2,
            height: translatedHeight,
            width: translatedWidth,
            radius: computeSize(1, 0, longestSide)
        };
    }

    /**
     * Computes a bounding box for a rotated arc.
     *
     * @param {number} width Width of the target area to fit the arc into.
     * @param {number} height Height of the target area to fit the arc into
     * @param {number} angleStart
     * @param {number} angleEnd
     * @returns {ArcBoundingBox}
     */
    module.computeBoundingBoxForArc = function computeBoundingBoxForArc(width, height, angleStart, angleEnd) {
        // Normalize angles.
        if (angleEnd - angleStart >= 2 * Math.PI) {
            angleStart = 0;
            angleEnd = 2 * Math.PI;
        } else {
            angleStart = angleStart % (Math.PI * 2);
            angleEnd = angleEnd % (Math.PI * 2);
        }

        return translateGaugeBoundingBox(computeNormalizedGaugeBoundingBox(angleStart, angleEnd), width, height);
    };

    /**
     * @summary
     * Returns the anchor coordinate on the edge of a rectangle at an angle.
     * The coordinate is relative to the center of the rectangle. The angle is
     * given by providing the sine and cosine of the angle.
     *
     * @description
     * The coordinates are computed by constructing a right-angled triangle
     * where the unknown to find runs along the x axis (y axis) and it's normal
     * is half the the height (width) of the rectangle. 𝛼 is given:
     * x(𝛼) = (width / 2) * cos(𝛼) / sin(𝛼)
     * y(𝛼) = (height / 2) * sin(𝛼) / cos(𝛼)
     * (can be proven using the sine law)
     * Depending on 𝛼, the hypotenuse will either end in one of the horizontal
     * (vertical) edges of the rectangle or intersect with one of the vertical
     * (horizontal) edges (the triangle either fits inside the rectangle or
     * not). In case it intersects, we can clamp that value to the x-(y-)
     * coordinates of the edge for a quick and dirty projection.
     *
     * @param {number} width Width of the rectangle.
     * @param {number} height Height of the rectangle.
     * @param {number} sin Sine of the anle.
     * @param {number} cos Cosine of the angle.
     * @returns {{x: number, y: number}}
     */
    module.getAnchorCoordinateForRect = function getAnchorCoordinateForRect(width, height, sin, cos) {
        const horizontal = width / 2,
            vertical = height / 2;

        return {
            x: clamp(-horizontal, horizontal * cos / Math.abs(sin), horizontal),
            y: clamp(-vertical, vertical * sin / Math.abs(cos), vertical)
        };
    };
})();

/**
 * Module to define and apply system defaults for handler- and system control configuration
 *
 * @module visuals/init/defaults
 */
(function() {
    var MODULE_NAME = "visuals.init.defaults",

        // MODULE CODE - START
        /** @lends module:visuals/init/defaults */
        module = shmi.pkg(MODULE_NAME);

    const HANDLER_CONFIG = [
            "visuals.handler.default.optionDialog", /* listens to 'optionDialog' event to handle option selection dialogs created using `shmi.optionDialog` */
            "visuals.handler.default.confirmation", /* listens to 'confirmation' event to handle confirmations created using `shmi.confirm` */
            "visuals.handler.default.notification", /* listens to 'notification' event to handle notifications sent using `shmi.notify` */
            "visuals.handler.default.passwordExpired", /* listens to 'password-expired' event to offer change of user password */
            "visuals.handler.default.connectionFailed", /* listens to 'connection-state' event to react to loss of websocket connection */
            "visuals.handler.default.keyboard", /* listens to 'keyboard-request' event to handle overlay keyboard */
            "visuals.handler.default.logout", /* listens to 'login-state' event to check 'loggedIn' state */
            "visuals.handler.default.numpad" /* listens to 'numpad-request' event to handle numeric input */
        ],
        CONTROLS_CONFIG = {
            "notificationDialog": { /* system notification dialog - `shmi.notify` */
                "name": "notification-dialog",
                "title": "${V_NOTIFICATION}",
                "template": "default/sys-dialog/dialog-notification"
            },
            "confirmDialog": { /* system confirmation dialog - `shmi.confirm` */
                "class-name": "dialog-box confirm",
                "title": "${V_CONFIRM_TITLE}",
                "name": "visuals-confirm-dialog",
                "template": "default/sys-dialog/dialog-confirm",
                "cover-background": true
            },
            "optionDialog": { /* system option selection dialog - `shmi.optionDialog` */
                "class-name": "dialog-box option",
                "title": "${V_OPTIONDIALOG_TITLE}",
                "name": "visuals-option-dialog",
                "template": "default/sys-dialog/dialog-option",
                "cover-background": true
            },
            "optionDialogButtons": {
                "class-name": "iq-button iq-variant-01",
                "template": "default/iq-button.iq-variant-01"
            },
            "passwordExpiredDialog": { /* system password change dialog (when expired) */
                "class-name": "dialog-box password-expired",
                "name": "password-expired-dialog",
                "template": "default/sys-dialog/dialog-password-expired",
                "title": "${V_PASSWORD_EXPIRED_TITLE}",
                "cover-background": true
            },
            "startupLogin": { /* system login configuration */
                "class-name": "login",
                "template": "default/login",
                "userEntry": "<div data-ui=\"user\" class=\"user\"><div class=\"userPic\"></div><div class=\"user_detail\"><h1 data-ui=\"user-name\"><%= USER %></h1></div></div>",
                "authBox": {
                    "class-name": "iq-variant-01 no-icon hidden",
                    "label": "Authentication Realm",
                    "template": "default/iq-select-box.iq-variant-01",
                    "selected": 0
                }
            },
            "loadingOverlay": { /* system loading overlay configuration */
                "tagName": "DIV",
                "class-name": "loading-box-overlay",
                "template": "default/loading-box-overlay"
            },
            "threadsDialogCreate": { /* system thread create dialog */
                "dialogTitle": "${threads.create.dialog.title}",
                "dialogClassName": "dialog-box threads-dialog",
                "dialogTemplate": "default/dialog-box",
                "titleClassName": "iq-input-field iq-variant-02 title",
                "titleTemplate": "default/iq-input-field.iq-variant-02",
                "titleLabel": "${threads.create.title.label}",
                "messageClassName": "iq-input-field iq-variant-02 message",
                "messageTemplate": "default/iq-input-field.iq-variant-02",
                "messageLabel": "${threads.create.message.label}",
                "deleteClassName": "iq-button iq-variant-01 delete",
                "deleteTemplate": "default/iq-button.iq-variant-01",
                "deleteLabel": "",
                "applyClassName": "iq-button iq-variant-01 apply",
                "applyTemplate": "default/iq-button.iq-variant-01",
                "applyLabel": "${threads.create.apply.label}",
                "enterMessageNotification": "${threads.enterMessage.notification}"
            },
            "threadsDialogEdit": { /* system thread edit dialog */
                "dialogTitle": "${threads.edit.dialog.title}",
                "dialogClassName": "dialog-box threads-dialog",
                "dialogTemplate": "default/dialog-box",
                "titleClassName": "iq-input-field iq-variant-02 title",
                "titleTemplate": "default/iq-input-field.iq-variant-02",
                "titleLabel": "${threads.edit.title.label}",
                "messageClassName": "iq-input-field iq-variant-02 message",
                "messageTemplate": "default/iq-input-field.iq-variant-02",
                "messageLabel": "${threads.edit.message.label}",
                "deleteClassName": "iq-button iq-variant-01 delete",
                "deleteTemplate": "default/iq-button.iq-variant-01",
                "deleteLabel": "${threads.edit.delete.label}",
                "deleteConfirmation": "${threads.edit.delete.confirmation}",
                "applyClassName": "iq-button iq-variant-01 apply",
                "applyTemplate": "default/iq-button.iq-variant-01",
                "applyLabel": "${threads.edit.apply.label}",
                "enterMessageNotification": "${threads.enterMessage.notification}"
            },
            "trendSetsDialogCreate": { /* system trend set create dialog */
                "dialogClassName": "dialog-box trend-sets-dialog",
                "dialogTemplate": "default/dialog-box",
                "dialogTitle": "${ui-action.trend-set-create.dialog.title}",
                "nameClassName": "iq-input-field iq-variant-02 title",
                "nameTemplate": "default/iq-input-field.iq-variant-02",
                "nameLabel": "${ui-action.trend-set-create.name.label}",
                "cancelClassName": "iq-button iq-variant-01 cancel",
                "cancelTemplate": "default/iq-button.iq-variant-01",
                "cancelLabel": "${ui-action.trend-set-create.cancel.label}",
                "applyClassName": "iq-button iq-variant-01 apply",
                "applyTemplate": "default/iq-button.iq-variant-01",
                "applyLabel": "${ui-action.trend-set-create.apply.label}",
                "enterNameMessage": "${ui-action.trend-set-create.enterName.message}",
                "enterNameTitle": "${ui-action.trend-set-create.enterName.title}",
                "nameExistsMessage": "${ui-action.trend-set-create.nameExists.message}",
                "nameExistsTitle": "${ui-action.trend-set-create.nameExists.title}"
            },
            "trendSetsDialogEdit": { /* system trend set edit dialog */
                "dialogClassName": "dialog-box trend-sets-dialog edit",
                "dialogTemplate": "default/dialog-box",
                "dialogTitle": "${ui-action.trend-set-rename.dialog.title}",
                "nameClassName": "iq-input-field iq-variant-02 title",
                "nameTemplate": "default/iq-input-field.iq-variant-02",
                "nameLabel": "${ui-action.trend-set-rename.name.label}",
                "cancelClassName": "iq-button iq-variant-01 cancel",
                "cancelTemplate": "default/iq-button.iq-variant-01",
                "cancelLabel": "${ui-action.trend-set-rename.cancel.label}",
                "applyClassName": "iq-button iq-variant-01 apply",
                "applyTemplate": "default/iq-button.iq-variant-01",
                "applyLabel": "${ui-action.trend-set-rename.apply.label}",
                "enterNameMessage": "${ui-action.trend-set-rename.enterName.message}",
                "enterNameTitle": "${ui-action.trend-set-rename.enterName.title}",
                "nameExistsMessage": "${ui-action.trend-set-rename.nameExists.message}",
                "nameExistsTitle": "${ui-action.trend-set-rename.nameExists.title}"
            },
            "trendExportDialog": {
                "dialogClassName": "dialog-box trend-data-export",
                "dialogTemplate": "default/dialog-box",
                "dialogTitle": "${ui-action.trend-export.dialog.title}",
                "startLabelClassName": "iq-label iq-variant-01 start-label",
                "startLabel": "${ui-action.trend-export.start.label}",
                "startClassName": "iq-button iq-variant-02 date-time-selection",
                "startTemplate": "default/iq-button.date-time-selection",
                "endLabelClassName": "iq-label iq-variant-01 end-label",
                "endLabel": "${ui-action.trend-export.end.label}",
                "endClassName": "iq-button iq-variant-02 date-time-selection",
                "endTemplate": "default/iq-button.date-time-selection",
                "applyClassName": "iq-button iq-variant-01 apply",
                "applyTemplate": "default/iq-button.iq-variant-01",
                "applyLabel": "${ui-action.trend-export.apply.label}",
                "cancelClassName": "iq-button iq-variant-01 cancel",
                "cancelTemplate": "default/iq-button.iq-variant-01",
                "cancelLabel": "${ui-action.trend-export.cancel.label}",
                "dateTimeFormat": "${iqtrend_timestamp_format}"
            },
            "dateTimeDialog": {
                "dialogClassName": "dialog-box date-time-selection",
                "dialogTemplate": "default/dialog-box",
                "dateLabelClassName": "iq-label date-label iq-variant-01",
                "dateLabel": "${date-time-selection.date.label}",
                "dateSelectClassName": "iq-select-date",
                "dateSelectTemplate": "default/iq-select-date.iq-variant-01",
                "timeLabelClassName": "iq-label time-label iq-variant-01",
                "timeLabel": "${date-time-selection.time.label}",
                "timeSelectClassName": "iq-select-time",
                "timeSelectTemplate": "default/iq-select-time.iq-variant-01",
                "applyClassName": "iq-button iq-variant-01",
                "applyTemplate": "default/iq-button.iq-variant-01",
                "applyLabel": "${date-time-selection.apply.label}",
                "cancelClassName": "iq-button iq-variant-01",
                "cancelTemplate": "default/iq-button.iq-variant-01",
                "cancelLabel": "${date-time-selection.cancel.label}"
            }
        };

    /** Default event handler configuration
     * @constant
     */
    module.HANDLER_CONFIG = HANDLER_CONFIG;

    /** Default system control configuration
     * @constant
     */
    module.CONTROLS_CONFIG = CONTROLS_CONFIG;
}());

/* default handler for confirmations */
(function() {
    /** @lends module:visuals/handler/default/confirmation */
    const module = shmi.pkg("visuals.handler.default.confirmation"),
        confirmationQueue = [];
    let dialog = null,
        callback = null;

    /**
     * get control for specified function
     *
     * @param {object[]} controls array of control instances
     * @param {string} func name of function
     * @returns {object|null} control instance for specified function
     */
    function getFunctionButton(controls, func) {
        let ret = null;
        if (Array.isArray(controls)) {
            for (let i = 0; i < controls.length; i++) {
                if (controls[i].element.getAttribute('data-function') === func) {
                    ret = controls[i];
                    break;
                } else if (Array.isArray(controls[i].controls)) {
                    ret = getFunctionButton(controls[i].controls, func);
                    if (ret) {
                        break;
                    }
                }
            }
        }
        return ret;
    }

    /**
     * get key pressed handler to close dialog with escape key
     *
     * @param {object} state handler state reference
     * @returns {function} handler function
     */
    function getOnKeyPress(state) {
        return function onKeyPress(e) {
            if (e.code === "Escape" && callback) {
                state.cbExecuted = true;
                runCallbackDelayed(callback, false);

                dialog.hide();
                deleteDialog(state);
            }
        };
    }

    /**
     * run confirmation callback decoupled
     *
     * @param {function} cb confirmation callback
     * @param {boolean} confirmed `true` if user confirmed request
     */
    function runCallbackDelayed(cb, confirmed) {
        shmi.decouple(function() {
            try {
                cb(confirmed);
            } catch (exc) {
                console.error("exception running shmi.confirm callback:", exc);
            }
        });
    }

    /**
     * initialize dialog content when dialog is enabled
     *
     * @param {string} msg confirmation message
     */
    function onDialogEnable(msg) {
        const state = {
                cbExecuted: false,
                keyHandler: null
            },
            confirmationTextElement = shmi.getElementByName('confirm-text', dialog.element);
        let confirmBtn = null,
            denyBtn = null;

        if (confirmationTextElement) {
            confirmationTextElement.textContent = msg;
        }

        confirmBtn = getFunctionButton(dialog.controls, "confirm");
        if (!confirmBtn) {
            console.error("confirm button missing in confirmation-dialog");
            callback(false);
            deleteDialog(state);

            return;
        }

        denyBtn = getFunctionButton(dialog.controls, "deny");
        if (!denyBtn) {
            console.error("deny button missing in confirmation-dialog");
            callback(false);
            deleteDialog(state);

            return;
        }

        confirmBtn.listen("click", function() {
            state.cbExecuted = true;
            runCallbackDelayed(callback, true);

            dialog.hide();
            deleteDialog(state);
        });

        denyBtn.listen("click", function() {
            state.cbExecuted = true;
            runCallbackDelayed(callback, false);

            dialog.hide();
            deleteDialog(state);
        });

        dialog.listen('open', function() {
            const tabElem = dialog.element.querySelector('[data-ui=button]');
            if (tabElem) {
                tabElem.focus();
            }
        });

        dialog.listen('close', function() {
            if (!state.cbExecuted) {
                state.cbExecuted = true;
                runCallbackDelayed(callback, false);
                deleteDialog(state);
            }
        });

        state.keyHandler = getOnKeyPress(state);

        dialog.element.addEventListener('keyup', state.keyHandler);

        dialog.show();
    }

    /**
     * create confirmation dialog
     *
     */
    function createDialog() {
        const currentEvent = confirmationQueue[0],
            title = currentEvent.detail.title,
            param = currentEvent.detail.param,
            msg = shmi.evalString(shmi.localize(currentEvent.detail.message), param),
            systemControlConfig = shmi.requires("visuals.session.SysControlConfig"),
            diaConfig = shmi.cloneObject(systemControlConfig.confirmDialog);

        callback = currentEvent.detail.callback;
        diaConfig.title = title || diaConfig.title;
        diaConfig.title = shmi.evalString(shmi.localize(diaConfig.title), param);

        dialog = shmi.createControl("dialog-box", document.body, diaConfig, 'DIV');

        shmi.onActive([dialog], () => {
            onDialogEnable(msg);
        });

        dialog.enable();
    }

    /**
     * cleanup & delete dialog after use
     *
     * @param {object} state handler state reference
     */
    function deleteDialog(state) {
        dialog.element.removeEventListener('keyup', state.keyHandler);
        shmi.deleteControl(dialog);
        dialog = null;
        confirmationQueue.shift(); //remove handled event

        shmi.decouple(function() {
            if (confirmationQueue.length && dialog === null) {
                createDialog();
            }
        });
    }

    /**
     * handle confirmation request event
     *
     * @param {object} event confirmation request event
     */
    function defaultConfirmationHandler(event) {
        try {
            shmi.requires("visuals.controls.iq-button");
            shmi.requires("visuals.controls.DialogBox");
        } catch (exc) {
            console.log("error trying to load controls: iq-button, DialogBox\n" + exc);
            return;
        }
        confirmationQueue.push(event);
        if (confirmationQueue.length === 1) {
            createDialog();
        }
    }

    let confirmationToken = null;

    /**
     * register event handler
     */
    module.register = function() {
        if (confirmationToken) {
            console.debug("confirmation handler was active!");
            confirmationToken.unlisten();
            confirmationToken = null;
        }
        confirmationToken = shmi.listen('confirmation', defaultConfirmationHandler);
    };

    /**
     * deregister event handler
     */
    module.deregister = function() {
        if (!confirmationToken) {
            console.debug("confirmation handler not active!");
        } else {
            confirmationToken.unlisten();
            confirmationToken = null;
        }
    };
}());

(function() {
    var activeId = null;

    function defaultConnectionFailed(event) {
        activeId = Date.now();
        var dismisToken = shmi.listen("notification-dismissed", function(evt) {
            dismisToken.unlisten();
            window.location.reload();
        }, { "detail.param.id": activeId });
        shmi.notify("${CONNECTION_RESET}", "${V_NOTIFICATION}", { id: activeId });
    }

    var module = shmi.pkg("visuals.handler.default.connectionFailed"),
        failedToken = null;

    module.register = function() {
        if (failedToken) {
            console.debug("connection-failed handler was active!");
            failedToken.unlisten();
            failedToken = null;
        }
        failedToken = shmi.listen("connection-failed", defaultConnectionFailed);
    };

    module.deregister = function() {
        if (!failedToken) {
            console.debug("connection-failed handler not active!");
        } else {
            failedToken.unlisten();
            failedToken = null;
        }
    };
}());

/**
 * Module to handle logout of user at application runtime
 *
 * @module visuals/handler/default/logout
 */
(function() {
    /** @lends module:visuals/handler/default/logout */
    const module = shmi.pkg("visuals.handler.default.logout");

    let loginToken = null,
        wasLoggedIn = false;

    /**
     * register - register handler module
     *
     */
    module.register = function() {
        if (!loginToken) {
            loginToken = shmi.listen("login-state", (evt) => {
                const reload = wasLoggedIn && evt.detail.loggedIn === false;

                wasLoggedIn = evt.detail.loggedIn; //update last login state before reload, in case reloading is canceled
                if (reload) {
                    window.location.reload();
                }
            });
        }
    };

    /**
     * deregister - de-register handler module
     *
     */
    module.deregister = function() {
        if (loginToken) {
            loginToken.unlisten();
            loginToken = null;
            wasLoggedIn = false;
        }
    };
}());

/* default handler for notifications */
(function() {
    /** @lends module:visuals/handler/default/notification */
    const module = shmi.pkg("visuals.handler.default.notification"),
        notificationQueue = [];
    let dialog = null,
        onKeyPress = null;

    /**
     * create notification dialog
     *
     */
    function createDialog() {
        const currentEvent = notificationQueue[0],
            title = currentEvent.detail.title,
            param = currentEvent.detail.param,
            systemControlConfig = shmi.requires("visuals.session.SysControlConfig"),
            diaConfig = shmi.cloneObject(systemControlConfig.notificationDialog);
        let msg = currentEvent.detail.message;

        diaConfig.title = title || diaConfig.title;
        diaConfig.title = shmi.evalString(shmi.localize(diaConfig.title), param ?? null);
        msg = shmi.evalString(shmi.localize(msg), param ?? null);
        dialog = shmi.createControl("dialog-box", document.body, diaConfig, 'DIV');

        const closeTok = dialog.listen("close", function(evt) {
            closeTok.unlisten();
            deleteDialog(evt);
            shmi.fire("notification-dismissed", { param: param, timestamp: Date.now() }, shmi.visuals.session);
        });

        // Closing notification on ESC
        onKeyPress = function(e) {
            if (e.code === "Escape") {
                closeTok.unlisten();
                deleteDialog(e);
                shmi.fire("notification-dismissed", { param: param, timestamp: Date.now() }, shmi.visuals.session);
            }
        };

        function onDialogEnable() {
            const notificationTextElement = shmi.getElementByName('notification-text', dialog.element);
            if (notificationTextElement) {
                notificationTextElement.textContent = msg;
            }

            dialog.listen('open', function() {
                const tabElem = dialog.element.querySelector('[data-ui=button]');
                if (tabElem) {
                    tabElem.focus();
                }
            });

            dialog.element.addEventListener('keyup', onKeyPress);

            dialog.show();
        }

        shmi.onActive([dialog], onDialogEnable);

        dialog.enable();
    }

    /**
     * clear created dialog
     *
     */
    function deleteDialog() {
        dialog.element.removeEventListener('keyup', onKeyPress);
        shmi.deleteControl(dialog);
        dialog = null;
        notificationQueue.shift(); //remove handle event
        shmi.decouple(function() {
            if (notificationQueue.length) {
                createDialog();
            }
        });
    }

    /**
     * handle notification event
     *
     * @param {object} event event reference
     */
    function defaultNotificationHandler(event) {
        try {
            shmi.requires("visuals.controls.iq-button");
            shmi.requires("visuals.controls.DialogBox");
        } catch (exc) {
            console.log("error trying to load controls: iq-button, DialogBox\n" + exc);
            return;
        }
        notificationQueue.push(event);
        if (notificationQueue.length === 1) {
            createDialog();
        }
    }

    let notificationToken = null;

    /**
     * register event handler
     */
    module.register = function() {
        if (notificationToken) {
            console.debug("notification handler was active!");
            notificationToken.unlisten();
            notificationToken = null;
        }
        notificationToken = shmi.listen('notification', defaultNotificationHandler);
    };

    /**
     * deregister event handler
     */
    module.deregister = function() {
        if (!notificationToken) {
            console.debug("notification handler not active!");
        } else {
            notificationToken.unlisten();
            notificationToken = null;
        }
    };
}());

/**
 * Default handler for 'numpad-request' events.
 *
 * @module visuals/handler/default/numpad
 */
(function() {
    /** @lends module:visuals/handler/default/numpad */
    const module = shmi.pkg("visuals.handler.default.numpad");
    let numpad = null,
        numpadToken = null,
        tokens = [];

    /**
     * getOnClose - get callback to run when numpad dialog is closed
     *
     * @param {object} currentEvent current request event
     * @returns {function} close callback
     */
    function getOnClose(currentEvent) {
        return () => {
            tokens.forEach((t) => {
                t.unlisten();
            });
            tokens = [];
            deleteNumpad();
        };
    }

    /**
     * createNumpad - creates numpad to handle request
     *
     */
    function createNumpad(event) {
        const numConfig = {},
            iter = shmi.requires("visuals.tools.iterate.iterateObject");

        iter(event.detail, function(val, key) {
            numConfig[key] = event.detail[key];
        });
        if (numConfig.label) {
            numConfig.label = shmi.evalString(shmi.localize(numConfig.label), event.detail.param);
        }

        numpad = shmi.createControl("numpad", document.body, numConfig, 'DIV', null, true);

        const onClose = getOnClose(event);

        tokens.push(numpad.listen("close", onClose));

        shmi.onActive([numpad], () => {
            numpad.show();
        });
    }

    /**
     * deleteNumpad - delete created numpad instance
     *
     */
    function deleteNumpad() {
        shmi.deleteControl(numpad);
        numpad = null;
    }

    /**
     * numpadRequestHandle - handle incoming request events
     *
     * @param {object} event numpad request event
     */
    function numpadRequestHandler(event) {
        if (numpad === null) {
            createNumpad(event);
        }
    }

    /**
     * register - register 'numpad-request' event handler
     *
     */
    module.register = function() {
        if (numpadToken) {
            console.debug("numpad handler was active!");
            numpadToken.unlisten();
            numpadToken = null;
        }
        numpadToken = shmi.listen('numpad-request', numpadRequestHandler);
    };

    /**
     * deregister - deregister 'numpad-request' event handler
     *
     */
    module.deregister = function() {
        if (!numpadToken) {
            console.debug("numpad handler not active!");
        } else {
            numpadToken.unlisten();
            numpadToken = null;
        }
    };
}());

/* default handler for option dialogs */
(function() {
    /** @lends module:visuals/handler/default/optionDialog */
    const module = shmi.pkg("visuals.handler.default.optionDialog"),
        optionDialogQueue = [];
    let optButtonTokens = [],
        dialog = null,
        cancelBtn = null,
        onKeyPress = null;

    /**
     * handle option dialog events
     *
     * @param {object} event event reference
     */
    function defaultOptionDialogHandler(event) {
        try {
            shmi.requires("visuals.controls.iq-button");
            shmi.requires("visuals.controls.DialogBox");
        } catch (exc) {
            console.log("error trying to load controls: iq-button, DialogBox\n" + exc);
            return;
        }

        optionDialogQueue.push(event);
        if (optionDialogQueue.length === 1) {
            createDialog();
        }
    }

    /**
     * create option dialog
     *
     */
    function createDialog() {
        const buttonElements = {},
            currentEvent = optionDialogQueue[0],
            dialogSettings = currentEvent.detail;

        if (!dialogSettings.buttons || !Array.isArray(dialogSettings.buttons) || typeof dialogSettings.buttons[0] !== 'object') {
            shmi.log('[shmi.optionDialog] Invalid options provided ', 3);
            return;
        }

        const systemControlConfig = shmi.requires("visuals.session.SysControlConfig"),
            diaConfig = shmi.cloneObject(systemControlConfig.optionDialog),
            buttonConfig = shmi.cloneObject(systemControlConfig.optionDialogButtons);

        diaConfig.title = dialogSettings.title || diaConfig.title;
        diaConfig.title = shmi.evalString(shmi.localize(diaConfig.title), dialogSettings.titleParams);
        // Add classes from "class-name" setting (if set) and filter out
        // duplicates.
        if (diaConfig["class-name"] || dialogSettings["class-name"]) {
            diaConfig["class-name"] = [
                ...new Set([
                    ...(diaConfig["class-name"] ? diaConfig["class-name"].split(/\s+/) : []),
                    ...(dialogSettings["class-name"] ? dialogSettings["class-name"].split(/\s+/) : [])
                ])
            ].filter((str) => str.length).join(" ");
        }
        dialog = shmi.createControl("dialog-box", document.body, diaConfig, 'DIV');

        onKeyPress = function(e) {
            if (e.code === "Escape" && cancelBtn.callback) {
                cancelBtn.callback(cancelBtn.name, e);
                dialog.hide();
                deleteDialog();
            }
        };

        function onDialogEnable() {
            optButtonTokens.forEach(function(token) {
                if (token && token.unlisten) {
                    token.unlisten();
                }
            });
            optButtonTokens = [];

            const optionDialogTextElement = dialog.element.querySelector('[data-name=option-text]');
            if (optionDialogTextElement) {
                const msgParams = dialogSettings.messageParams || {};
                dialogSettings.message = shmi.evalString(shmi.localize(dialogSettings.message), msgParams);
                optionDialogTextElement.textContent = dialogSettings.message;
            }

            const dialogOptionsContainer = dialog.element.querySelector('[data-name=dialog-options]'),
                dialogControlsContainer = dialog.element.querySelector('[data-name=dialog-ctrls]');

            dialogSettings.buttons.forEach(function(btnConfig) {
                const btnName = btnConfig.config.name,
                    btnCb = btnConfig.callback;
                let insertNode = null;

                switch (btnConfig.type) {
                case 'warning':
                case 'success':
                case 'option':
                    if (dialogSettings.buttonLayout === 'vertical') {
                        if (dialogOptionsContainer) {
                            insertNode = dialogOptionsContainer;
                        } else {
                            shmi.log('[shmi.optionDialog] HTML Element not found: data-name="dialog-options"', 3);
                            return;
                        }
                    } else if (dialogSettings.buttonLayout === 'horizontal') {
                        if (dialogControlsContainer) {
                            insertNode = dialogControlsContainer;
                        } else {
                            shmi.log('[shmi.optionDialog] HTML Element not found: data-name="dialog-ctrls"', 3);
                            return;
                        }
                    }
                    break;
                case 'cancel':
                    if (!dialogControlsContainer) {
                        shmi.log('[shmi.optionDialog] HTML Element not found: data-name="dialog-ctrls"', 3);
                        return;
                    } else {
                        insertNode = dialogControlsContainer;
                    }
                    break;
                default:
                    return;
                }

                if (insertNode) {
                    btnConfig.config = Object.assign({}, buttonConfig, btnConfig.config);
                    btnConfig.config['class-name'] = btnConfig.config['class-name'] ? btnConfig.config['class-name'] + ' ' + btnConfig.type : btnConfig.type;
                    buttonElements[btnName] = shmi.createControl("iq-button", insertNode, btnConfig.config, 'DIV', "before");
                    optButtonTokens.push(buttonElements[btnName].listen("click", function(event) {
                        dialog.hide();
                        deleteDialog();
                        try {
                            btnCb(btnName, event);
                        } catch (exc) {
                            console.error("exception running shmi.optionDialog callback:", exc);
                        }
                    }));
                }
            });

            dialog.listen('open', function() {
                const tabElem = dialog.element.querySelector('[data-ui=button]');
                if (tabElem) {
                    tabElem.focus();
                }
            });

            cancelBtn = dialogSettings.buttons.find(function(btn) {
                return btn.type === 'cancel';
            });

            if (cancelBtn) {
                // If cancel button exists, call cancel callback on pressing ESC
                dialog.element.addEventListener('keyup', onKeyPress);
            }

            dialog.show();
        }

        shmi.onActive([dialog], onDialogEnable);

        dialog.enable();
    }

    /**
     * clear created dialog
     *
     */
    function deleteDialog() {
        dialog.element.removeEventListener('keyup', onKeyPress);
        shmi.deleteControl(dialog);
        dialog = null;
        optionDialogQueue.shift(); //remove handle event
        optButtonTokens.forEach(function(token) {
            if (token && token.unlisten) {
                token.unlisten();
            }
        });
        optButtonTokens = [];
        shmi.decouple(function() {
            if (optionDialogQueue.length && dialog === null) {
                createDialog();
            }
        });
    }

    let optionDialogToken = null;

    /**
     * register event handler
     */
    module.register = function() {
        if (optionDialogToken) {
            console.debug("option handler was active!");
            optionDialogToken.unlisten();
            optionDialogToken = null;
        }
        optionDialogToken = shmi.listen('optionDialog', defaultOptionDialogHandler);
    };

    /**
     * deregister event handler
     */
    module.deregister = function() {
        if (!optionDialogToken) {
            console.debug("option handler not active!");
        } else {
            optionDialogToken.unlisten();
            optionDialogToken = null;
        }
    };
}());

/* default handler for password expiration */
(function() {
    /** @lends module:visuals/handler/default/passwordExpired */
    const module = shmi.pkg("visuals.handler.default.passwordExpired");
    let tokens = [],
        dialog = null,
        parserReady = false,
        initialLoginExpired = function() {};

    /**
     * check if user input is valid & apply password change
     *
     * @param {object} refs resolved control references
     */
    function checkValid(refs) {
        const um = shmi.requires("visuals.session.UserManager");
        let currentPwd = null,
            newPwd = null;

        if (refs.newpw.getValue() !== refs.repeatpw.getValue()) {
            shmi.notify("${V_PASSWORD_REPEAT_NOT_EQUAL}");
            shmi.addClass(refs.repeatpw.element, "error");
            return;
        }

        refs.apply.lock();

        currentPwd = refs.curpw.getValue();
        newPwd = refs.newpw.getValue();
        um.changePassword(currentPwd, newPwd, function(response, error) {
            if (!error) {
                dialog.hide();
                return;
            }
            refs.apply.unlock();
            shmi.addClass(refs.curpw.element, "error");

            if (error.category === "auth-generic" && error.errc === 3) {
                shmi.addClass(refs.curpw.element, "error");
            } else if (error.category === "auth-generic" && error.errc === 5) {
                shmi.addClass(refs.newpw.element, "error");
            }

            if (error.localizedMessage) {
                shmi.notify(error.localizedMessage);
            } else {
                shmi.notify("${V_INVALID_PASSWORD}");
            }
        });
    }

    /**
     * reset error states for user input controls
     *
     * @param {object} refs resolved control references
     */
    function resetControls(refs) {
        shmi.removeClass(refs.curpw.element, "error");
        shmi.removeClass(refs.newpw.element, "error");
        shmi.removeClass(refs.repeatpw.element, "error");
    }

    /**
     * initialize resolved controls
     *
     * @param {object} refs resolved control references
     */
    function initControls(refs) {
        const ses = shmi.visuals.session,
            um = ses.UserManager;

        refs.username.setValue(um.currentUser.name);
        tokens.push(refs.cancel.listen("click", function() {
            dialog.hide();
        }));
        tokens.push(refs.apply.listen("click", function() {
            resetControls(refs);
            checkValid(refs);
        }));
    }

    /**
     * create password expired dialog
     */
    function createDialog() {
        if (!parserReady) {
            initialLoginExpired = function() {
                createDialog();
            };
            return;
        }

        const systemControlConfig = shmi.requires("visuals.session.SysControlConfig"),
            diaConfig = shmi.cloneObject(systemControlConfig.passwordExpiredDialog);
        diaConfig.title = diaConfig.title || "${V_PASSWORD_EXPIRED_TITLE}";
        diaConfig.title = shmi.localize(diaConfig.title);
        dialog = shmi.createControl("dialog-box", document.body, diaConfig, 'DIV');

        function onDialogEnable() {
            dialog.show();
            const ctrlNames = [
                ".current-user",
                ".current-pwd",
                ".new-pwd",
                ".repeat-pwd",
                ".apply-btn",
                ".cancel-btn"
            ];
            ctrlNames.forEach(function(cn, idx) {
                ctrlNames[idx] = dialog.getName() + " " + cn;
            });

            tokens.push(dialog.listen("close", () => {
                deleteDialog();
            }));

            shmi.onEnable(ctrlNames, function(refs) {
                initControls({
                    username: refs[ctrlNames[0]],
                    curpw: refs[ctrlNames[1]],
                    newpw: refs[ctrlNames[2]],
                    repeatpw: refs[ctrlNames[3]],
                    apply: refs[ctrlNames[4]],
                    cancel: refs[ctrlNames[5]]
                });
            });
        }

        shmi.onActive([dialog], onDialogEnable);

        dialog.enable();
    }

    /**
     * delete created dialog
     */
    function deleteDialog() {
        shmi.deleteControl(dialog);
        tokens.forEach(function(t) {
            t.unlisten();
        });
        tokens = [];
        dialog = null;
    }

    /**
     * handle password expired event
     *
     * @param {object} event event reference
     */
    function defaultPasswordExpiredHandler(event) {
        try {
            shmi.requires("visuals.controls.iq-button");
            shmi.requires("visuals.controls.DialogBox");
        } catch (exc) {
            console.log("error trying to load controls: Button, DialogBox\n" + exc);
            return;
        }

        if (dialog === null) {
            createDialog();
        }
    }

    let expiredToken = null;

    shmi.listenOnce("parser-ready", function() {
        parserReady = true;

        shmi.decouple(initialLoginExpired);
    });

    /**
     * register event handler
     */
    module.register = function() {
        if (expiredToken) {
            console.debug("password-expired handler was active!");
            expiredToken.unlisten();
            expiredToken = null;
        }
        expiredToken = shmi.listen("password-expired", defaultPasswordExpiredHandler);
    };

    /**
     * deregister event handler
     */
    module.deregister = function() {
        if (!expiredToken) {
            console.debug("password-expired handler not active!");
        } else {
            expiredToken.unlisten();
            expiredToken = null;
        }
    };
}());

(function() {
    /**
     * UI-Action `audit-backup` to export the audit log without clearing it.
     *
     * @version 1.0
     */
    const MODULE_NAME = "visuals.ui-actions.audit-backup",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-backup-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    function makeButtonConfig(buttonName) {
        return {
            "ui": "iq-button",
            "config": {
                "class-name": "iq-button iq-variant-01",
                "template": "default/iq-button.iq-variant-01",
                "label": `\${ui-action.audit-export.dialog.buttons.${buttonName.toLocaleLowerCase()}}`,
                "_controllers_": getControllerConfig(`button${buttonName}`)
            }
        };
    }

    function makeDialogController(resolve, reject) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonBackup: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {},
            onDisable: function(state) {},
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonBackup":
                case "buttonCancel":
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(slot.replace("button", ""));
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                resolved: false
            }
        };
    }

    function makeDialogLayout(resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "messageDialog",
                "class-name": "dialog-box option",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${ui-action.audit-backup.dialog.title}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(resolve, reject),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": [
                        {
                            "ui": "iq-text",
                            "config": {
                                "class-name": "iq-text iq-variant-01 message-text",
                                "template": "default/iq-text.iq-variant-01",
                                "name": "messageText",
                                "ui": "iq-text",
                                "text": "${ui-action.audit-backup.dialog.primaryText}"
                            }
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container dialog-button-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer"
                            },
                            "children": [
                                makeButtonConfig("Backup"),
                                makeButtonConfig("Cancel")
                            ]
                        }
                    ]
                }
            ]
        };
    }

    function createDialog() {
        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(resolve, reject);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                }
            });
        });
    }

    module.createDialog = createDialog;

    // MODULE CODE - END

    fLog("module loaded");
})();

(function() {
    /**
     * UI-Action `audit-download-latest` to download the latest audit log export.
     *
     * @version 1.0
     */
    const MODULE_NAME = "visuals.ui-actions.audit-download-latest",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-download-latest-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    function makeButtonConfig(buttonName) {
        return {
            "ui": "iq-button",
            "config": {
                "class-name": "iq-button iq-variant-01",
                "template": "default/iq-button.iq-variant-01",
                "label": `\${ui-action.audit-export.dialog.buttons.${buttonName.toLocaleLowerCase()}}`,
                "_controllers_": getControllerConfig(`button${buttonName}`)
            }
        };
    }

    function makeDialogController(resolve, reject) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonDownload: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {},
            onDisable: function(state) {},
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonDownload":
                case "buttonCancel":
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(slot.replace("button", ""));
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                resolved: false
            }
        };
    }

    function makeDialogLayout(resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "messageDialog",
                "class-name": "dialog-box option",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${ui-action.audit-download-latest.dialog.title}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(resolve, reject),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": [
                        {
                            "ui": "iq-text",
                            "config": {
                                "class-name": "iq-text iq-variant-01 message-text",
                                "template": "default/iq-text.iq-variant-01",
                                "name": "messageText",
                                "ui": "iq-text",
                                "text": "${ui-action.audit-download-latest.dialog.primaryText}"
                            }
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container dialog-button-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer"
                            },
                            "children": [
                                makeButtonConfig("Download"),
                                makeButtonConfig("Cancel")
                            ]
                        }
                    ]
                }
            ]
        };
    }

    function createDialog() {
        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(resolve, reject);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                }
            });
        });
    }

    module.createDialog = createDialog;

    // MODULE CODE - END

    fLog("module loaded");
})();

(function() {
    /**
     * UI-Action `audit-export` to export audit log.
     *
     * @version 1.0
     */
    const MODULE_NAME = "visuals.ui-actions.audit-export",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-export-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    function makeButtonConfig(buttonName) {
        return {
            "ui": "iq-button",
            "config": {
                "class-name": "iq-button iq-variant-01",
                "template": "default/iq-button.iq-variant-01",
                "label": `\${ui-action.audit-export.dialog.buttons.${buttonName.toLocaleLowerCase()}}`,
                "_controllers_": getControllerConfig(`button${buttonName}`)
            }
        };
    }

    function makeDialogController(resolve, reject) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonExport: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {},
            onDisable: function(state) {},
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonExport":
                case "buttonCancel":
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(slot.replace("button", ""));
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                resolved: false
            }
        };
    }

    function makeDialogLayout(resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "messageDialog",
                "class-name": "dialog-box option",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${ui-action.audit-export.dialog.title}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(resolve, reject),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": [
                        {
                            "ui": "iq-text",
                            "config": {
                                "class-name": "iq-text iq-variant-01 message-text",
                                "template": "default/iq-text.iq-variant-01",
                                "name": "messageText",
                                "ui": "iq-text",
                                "text": "${ui-action.audit-export.dialog.primaryText}"
                            }
                        },
                        {
                            "ui": "iq-text",
                            "config": {
                                "class-name": "iq-text iq-variant-01 bold",
                                "template": "default/iq-text.iq-variant-01",
                                "name": "warningText",
                                "ui": "iq-text",
                                "text": "${ui-action.audit-export.dialog.exportWarningText}"
                            }
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container dialog-button-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer"
                            },
                            "children": [
                                makeButtonConfig("Export"),
                                makeButtonConfig("Cancel")
                            ]
                        }
                    ]
                }
            ]
        };
    }

    function createDialog() {
        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(resolve, reject);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                }
            });
        });
    }

    module.createDialog = createDialog;

    // MODULE CODE - END

    fLog("module loaded");
})();

(function() {
    /**
     * Layout module for the dialog displayed on audit storage events.
     *
     * @version 1.0
     */
    const MODULE_NAME = "visuals.tools.onAuditEvent",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME); //eslint-disable-line

    const CONTROLLER_NAME = "audit-storage-notification-dialog";

    // MODULE CODE - START

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    function makeButtonConfig(buttonName) {
        return {
            "ui": "iq-button",
            "config": {
                "class-name": "iq-button iq-variant-01",
                "template": "default/iq-button.iq-variant-01",
                "label": `\${audit-storage-notification.dialog.buttons.${buttonName.toLocaleLowerCase()}}`,
                "_controllers_": getControllerConfig(`button${buttonName}`)
            }
        };
    }

    function makeDialogController(isStorageFull, resolve, reject) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonExport: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonIgnore: {
                    ui: "iq-button",
                    events: ["click"],
                    optional: isStorageFull
                },
                buttonLogout: {
                    ui: "iq-button",
                    events: ["click"],
                    optional: !isStorageFull
                }
            },
            onChange: function(state, detail) {},
            onEnable: function(state) {},
            onDisable: function(state) {
                if (!state.data.resolved) {
                    state.data.resolved = true;
                    resolve(null);
                }
            },
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonExport":
                case "buttonIgnore":
                case "buttonLogout":
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(slot.replace("button", ""));
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                resolved: false
            }
        };
    }

    function makeDialogLayout(isStorageFull, isRetry, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "messageDialog",
                "class-name": "dialog-box option audit-storage-notification",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": isStorageFull ? "${audit-storage-notification.dialog.storageFullTitle}" : "${audit-storage-notification.dialog.storageWarningTitle}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(isStorageFull, resolve, reject),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": [
                        {
                            "ui": "iq-text",
                            "config": {
                                "class-name": "iq-text iq-variant-01 message-text",
                                "template": "default/iq-text.iq-variant-01",
                                "name": "messageText",
                                "ui": "iq-text",
                                "text": !isRetry ? (
                                    isStorageFull ?
                                        "${audit-storage-notification.dialog.storageFullText}" :
                                        "${audit-storage-notification.dialog.storageWarningText}"
                                ) : (
                                    isStorageFull ?
                                        "${audit-storage-notification.dialog.storageFullRetryText}" :
                                        "${audit-storage-notification.dialog.storageWarningRetryText}"
                                )
                            }
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container dialog-button-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer"
                            },
                            "children": isStorageFull ? [
                                makeButtonConfig("Export"),
                                makeButtonConfig("Logout")
                            ] : [
                                makeButtonConfig("Export"),
                                makeButtonConfig("Ignore")
                            ]
                        }
                    ]
                }
            ]
        };
    }

    function createDialog(isStorageFull, isRetry, onCreated = null) {
        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(isStorageFull, isRetry, resolve, reject);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                    onCreated?.(controls[0]);
                }
            });
        });
    }

    module.createDialog = createDialog;

    // MODULE CODE - END

    fLog("module loaded");
})();

/**
 * Module to implement dialog to retrieve date & time input
 *
 * @module visuals/tools/date/dateTimeDialog
 */
(function() {
    const MODULE_NAME = "visuals.tools.date.dateTimeDialog",
        /** @lends module:visuals/tools/date/dateTimeDialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "date-time-selection-dialog";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * check if required widgets are ready
     *
     * @param {object} state controller state
     * @return {boolean} `true` if widgets are ready, `false` else
     */
    function widgetsReady(state) {
        return [
            "selectDate",
            "selectTime",
            "buttonApply",
            "buttonCancel"
        ].every((slotName) => !!state.getInstance(slotName));
    }

    /**
     * set controller slot values
     *
     * @param {object} state controller state
     * @param {object} timestamp slot data
     */
    function setTimestamp(state, timestamp) {
        const secondsTimestamp = Math.floor(timestamp / 1000);
        state.data.ignoreEvents = true;
        state.getInstance("selectDate").setValue(secondsTimestamp);
        state.getInstance("selectTime").setValue(secondsTimestamp);
        state.data.ignoreEvents = false;
    }

    /**
     * get controller slot values
     *
     * @param {object} state controller state
     * @return {object} slot data
     */
    function getTimestamp(state) {
        return state.getInstance("selectTime").getValue() * 1000;
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {object} options dialog options
     * @param {number} options.timestamp initial timestamp in ms
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.dateLabelClassName] class-name to apply to date label
     * @param {string} [options.dateLabelTemplate] template to use for date label
     * @param {string} [options.dateLabel] label text for date label
     * @param {string} [options.dateSelectClassName] class-name to apply to select-date
     * @param {string} [options.dateSelectTemplate] template to use for select-date
     * @param {string} [options.timeLabelClassName] class-name to apply to time label
     * @param {string} [options.timeLabelTemplate] template to use for time label
     * @param {string} [options.timeLabel] label text for time label
     * @param {string} [options.timeSelectClassName] class-name to apply to select-time
     * @param {string} [options.timeSelectTemplate] template to use for select-time
     * @param {string} [options.applyClassName] class-name to apply to apply button
     * @param {string} [options.applyTemplate] template to use for apply button
     * @param {string} [options.applyLabel] label text for apply button
     * @param {string} [options.cancelClassName] class-name to apply to cancel button
     * @param {string} [options.cancelTemplate] template to use for cancel button
     * @param {string} [options.cancelLabel] label text for cancel button
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(options, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                selectDate: {
                    ui: "iq-select-date",
                    optional: true,
                    events: ["change"]
                },
                selectTime: {
                    ui: "iq-select-time",
                    optional: true,
                    events: ["change"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {
                if (widgetsReady(state) && !state.data.contentSet) {
                    state.data.contentSet = true;
                    setTimestamp(state, options.timestamp ?? Date.now());
                }
            },
            onEnable: function(state) {

            },
            onDisable: function(state) {

            },
            onEvent: function(state, slot, type, event) {
                if (state.data.ignoreEvents) {
                    return;
                }
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "selectDate":
                    state.data.ignoreEvents = true;
                    state.getInstance("selectTime").setValue(state.getInstance("selectDate").getValue());
                    state.data.ignoreEvents = false;
                    break;
                case "selectTime":
                    state.data.ignoreEvents = true;
                    state.getInstance("selectDate").setValue(state.getInstance("selectTime").getValue());
                    state.data.ignoreEvents = false;
                    break;
                case "buttonCancel":
                    state.data.resolved = true;
                    resolve(null);
                    state.getInstance("dialog").hide();
                    break;
                case "buttonApply":
                    state.data.resolved = true;
                    resolve(getTimestamp(state));
                    state.getInstance("dialog").hide();
                    break;
                default:
                }
            },
            data: {
                resolved: false,
                contentSet: false,
                ignoreEvents: false
            }
        };
    }

    /**
     * get configured ui layout for date/time input dialog
     *
     * @param {object} options dialog options
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(options, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "dateDialog",
                "class-name": options.dialogClassName || "dialog-box date-time-selection",
                "template": options.dialogTemplate || "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": options.dialogTitle || "Select Date and Time",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(options, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container date-time-content",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": false,
                                "flex-wrap": true,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "containerDateTime",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "container",
                                    "config": {
                                        "class-name": "iq-container date-container",
                                        "type": "iqflex",
                                        "flex-orientation": "column",
                                        "flex-none": true,
                                        "flex-wrap": false,
                                        "flex-primary-align": "start",
                                        "flex-secondary-align": "stretch",
                                        "flex-line-align": "start",
                                        "name": "containerDate",
                                        "template": null
                                    },
                                    "children": [
                                        {
                                            "ui": "iq-label",
                                            "config": {
                                                "class-name": options.dateLabelClassName || "iq-label date-label iq-variant-01",
                                                "name": "labelDate",
                                                "ui": "iq-label",
                                                "item": null,
                                                "type": null,
                                                "auto-type": true,
                                                "decimal-delimiter": ".",
                                                "precision": 2,
                                                "auto-precision": true,
                                                "options": [],
                                                "pattern": null,
                                                "tooltip": null,
                                                "value-as-tooltip": false,
                                                "text": options.dateLabel || "Date"
                                            },
                                            "children": []
                                        },
                                        {
                                            "ui": "iq-select-date",
                                            "config": {
                                                "class-name": options.dateSelectClassName || "iq-select-date",
                                                "template": options.dateSelectTemplate || "default/iq-select-date.iq-variant-01",
                                                "name": "selectDate",
                                                "ui": "iq-select-date",
                                                "label": "",
                                                "item": null,
                                                "auto-label": true,
                                                "dateformat": "${V_DATEFORMAT}",
                                                "show-icon": true,
                                                "icon-src": "pics/system/controls/iq-select-date/calendar-icon.svg",
                                                "icon-class": null,
                                                "icon-title": null,
                                                "inline-svg": true,
                                                "_controllers_": getControllerConfig("selectDate")
                                            },
                                            "children": []
                                        }
                                    ]
                                },
                                {
                                    "ui": "container",
                                    "config": {
                                        "class-name": "iq-container time-container",
                                        "type": "iqflex",
                                        "flex-orientation": "column",
                                        "flex-none": true,
                                        "flex-wrap": false,
                                        "flex-primary-align": "start",
                                        "flex-secondary-align": "stretch",
                                        "flex-line-align": "start",
                                        "name": "containerTime",
                                        "template": null
                                    },
                                    "children": [
                                        {
                                            "ui": "iq-label",
                                            "config": {
                                                "class-name": options.timeLabelClassName || "iq-label time-label iq-variant-01",
                                                "name": "labelTime",
                                                "ui": "iq-label",
                                                "item": null,
                                                "type": null,
                                                "auto-type": true,
                                                "decimal-delimiter": ".",
                                                "precision": 2,
                                                "auto-precision": true,
                                                "options": [],
                                                "pattern": null,
                                                "tooltip": null,
                                                "value-as-tooltip": false,
                                                "text": options.timeLabel || "Time"
                                            },
                                            "children": []
                                        },
                                        {
                                            "ui": "container",
                                            "config": {
                                                "class-name": "iq-container time-content",
                                                "type": "iqflex",
                                                "flex-orientation": "column",
                                                "flex-none": true,
                                                "flex-wrap": false,
                                                "flex-primary-align": "center",
                                                "flex-secondary-align": "stretch",
                                                "flex-line-align": "start",
                                                "name": "containerTime",
                                                "template": null
                                            },
                                            "children": [
                                                {
                                                    "ui": "iq-select-time",
                                                    "config": {
                                                        "class-name": options.timeSelectClassName || "iq-select-time",
                                                        "template": options.timeSelectTemplate || "default/iq-select-time.iq-variant-01",
                                                        "name": "selectTime",
                                                        "ui": "iq-select-time",
                                                        "label": "",
                                                        "item": null,
                                                        "auto-label": true,
                                                        "numpad-enabled": false,
                                                        "isUTC": false,
                                                        "icon-src": "pics/system/controls/iq-select-time/clock-icon.svg",
                                                        "inline-svg": true,
                                                        "_controllers_": getControllerConfig("selectTime")
                                                    },
                                                    "children": []
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "containerFooter",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.cancelClassName || "iq-button iq-variant-01",
                                        "template": options.cancelTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "buttonCancel",
                                        "ui": "iq-button",
                                        "label": options.cancelLabel || "Cancel",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonCancel")
                                    },
                                    "children": []
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.applyClassName || "iq-button iq-variant-01 filled",
                                        "template": options.applyTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "buttonApply",
                                        "ui": "iq-button",
                                        "label": options.applyLabel || "Apply",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonApply")
                                    },
                                    "children": []
                                }
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create date and time selection dialog
     *
     * @param {object} options dialog options
     * @return {Promise<string|null>} promise resolving to timestamp or `null` if closed without selection
     */
    module.getDialog = (options) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(options, resolve, reject);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, [ dialog ]) => {
                    if (err) {
                        reject(err);
                    } else {
                        dialog.show();
                    }
                });
            } else {
                reject(new Error("Dialog layout could not be created."));
            }
        }
    });
}());

/**
 * Module implementing the dialog used by the `export-alarm-history` UI-Action.
 *
 * @module visuals/ui-actions/export-alarm-history/dialog
 */
(function() {
    const MODULE_NAME = "visuals.ui-actions.export-alarm-history.dialog",
        /** @lends module:visuals/ui-actions/export-alarm-history/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "export-alarm-history-dialog";
    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * Returns the widget configuration for button for one of the dialog's
     * user options.
     *
     * @param {string} buttonName Name of the button.
     * @param {string[]} [additionalCssClasses] Additional CSS classes to
     *  create the button with.
     * @returns {object}
     */
    function makeButtonConfig(buttonName, additionalCssClasses) {
        return {
            "ui": "iq-button",
            "config": {
                "class-name": ["iq-button", "iq-variant-01", ...(additionalCssClasses ?? [])].join(" "),
                "template": "default/iq-button.iq-variant-01",
                "label": `\${ui-action.export-alarm-history.dialog.buttons.${buttonName.toLocaleLowerCase()}}`,
                "_controllers_": getControllerConfig(`button${buttonName}`)
            }
        };
    }

    const buttonHandlers = {
        onSelectStartDate: async function onSelectStartDate(controllerState) {
            const { requestDateAndTime, formatDateTime } = shmi.requires("visuals.tools.date"),
                btn = controllerState.getInstance("buttonSelectStartDate");

            let initialTimestamp = controllerState.data.startTime;
            if (initialTimestamp <= 0) {
                const date = new Date;
                date.setMonth(0, 1);
                date.setHours(0, 0, 0, 0);
                initialTimestamp = date.getTime();
            }

            const timestamp = await requestDateAndTime({ timestamp: initialTimestamp });
            if (timestamp === null) {
                return;
            }

            controllerState.data.startTime = timestamp;
            if (timestamp <= 0) {
                btn.setLabel("${ui-action.export-alarm-history.dialog.startTimeInitial.label}");
            } else {
                btn.setLabel(formatDateTime(new Date(timestamp), {
                    datestring: shmi.localize("${alarmlist_dateformat}")
                }));
            }
        },
        onSelectEndDate: async function onSelectEndDate(controllerState) {
            const { requestDateAndTime, formatDateTime } = shmi.requires("visuals.tools.date"),
                btn = controllerState.getInstance("buttonSelectEndDate");

            const timestamp = await requestDateAndTime({ timestamp: controllerState.data.endTime });
            if (timestamp === null) {
                return;
            }

            controllerState.data.endTime = timestamp;
            btn.setLabel(formatDateTime(new Date(timestamp), {
                datestring: shmi.localize("${alarmlist_dateformat}")
            }));
        }
    };

    /**
     * Creates a controller configuration for a new dialog.
     *
     * @param {function} resolve Function called upon user confirmation or
     *  cancellation.
     * @returns {object}
     */
    function makeDialogController(resolve) {
        const defaultStartTime = new Date(0),
            defaultEndTime = new Date;
        defaultEndTime.setHours(23, 59, 59, 999);

        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonExport: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonSelectStartDate: {
                    ui: "iq-button",
                    events: ["click"]
                },
                buttonSelectEndDate: {
                    ui: "iq-button",
                    events: ["click"]
                },
                selectSeverityInfo: {
                    ui: "iq-checkbox",
                    events: ["change"]
                },
                selectSeverityWarning: {
                    ui: "iq-checkbox",
                    events: ["change"]
                },
                selectSeverityAlarm: {
                    ui: "iq-checkbox",
                    events: ["change"]
                }
            },
            onChange: function(state, detail) { },
            onEnable: function(state) {
                const { formatDateTime } = shmi.requires("visuals.tools.date");

                if (state.data.startTime <= 0) {
                    state.getInstance("buttonSelectStartDate").setLabel("${ui-action.export-alarm-history.dialog.startTimeInitial.label}");
                } else {
                    state.getInstance("buttonSelectStartDate").setLabel(formatDateTime(new Date(state.data.startTime), {
                        datestring: shmi.localize("${alarmlist_dateformat}")
                    }));
                }

                state.getInstance("buttonSelectEndDate").setLabel(formatDateTime(new Date(state.data.endTime), {
                    datestring: shmi.localize("${alarmlist_dateformat}")
                }));

                state.getInstance("selectSeverityInfo").setValue(state.data.selectedTypes.has("info") ? 1 : 0);
                state.getInstance("selectSeverityWarning").setValue(state.data.selectedTypes.has("warning") ? 1 : 0);
                state.getInstance("selectSeverityAlarm").setValue(state.data.selectedTypes.has("alarm") ? 1 : 0);
            },
            onDisable: function(state) { },
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "selectSeverityInfo":
                    if (event.detail.value) {
                        state.data.selectedTypes.add("info");
                    } else {
                        state.data.selectedTypes.delete("info");
                    }
                    break;
                case "selectSeverityWarning":
                    if (event.detail.value) {
                        state.data.selectedTypes.add("warning");
                    } else {
                        state.data.selectedTypes.delete("warning");
                    }
                    break;
                case "selectSeverityAlarm":
                    if (event.detail.value) {
                        state.data.selectedTypes.add("alarm");
                    } else {
                        state.data.selectedTypes.delete("alarm");
                    }
                    break;
                case "buttonSelectStartDate":
                    buttonHandlers.onSelectStartDate(state);
                    break;
                case "buttonSelectEndDate":
                    buttonHandlers.onSelectEndDate(state);
                    break;
                case "buttonExport":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve({
                            startTime: state.data.startTime,
                            endTime: state.data.endTime,
                            selectedTypes: Array.from(state.data.selectedTypes)
                        });
                        state.getInstance("dialog").hide();
                    }
                    break;
                case "buttonCancel":
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                startTime: defaultStartTime.getTime(),
                endTime: defaultEndTime.getTime(),
                selectedTypes: new Set(["alarm", "warning", "info"]),
                resolved: false
            }
        };
    }

    /**
     * Generates a widget tree configuration for the dialog to be passed to
     * `BaseControl.addControl()`.
     *
     * @param {function} resolve Function called upon user confirmation or
     *  cancellation.
     * @returns {object}
     */
    function makeDialogLayout(resolve) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "auditLogViewerFilterDialog",
                "class-name": "dialog-box option alarm-history-export-dialog",
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${ui-action.export-alarm-history.dialog.title}",
                "content-template": null,
                "tab-limit": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": makeDialogController(resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container grid-spacing main-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content"
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container grid-spacing",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": false
                            },
                            "children": [
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01 filterlabel",
                                        "template": "default/iq-text.iq-variant-01",
                                        "text": "${ui-action.export-alarm-history.dialog.timeSelect.label}"
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-02 date-time-selection",
                                        "template": "default/iq-button.date-time-selection",
                                        "label": "uninitialized",
                                        "_controllers_": getControllerConfig("buttonSelectStartDate")
                                    }
                                },
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01",
                                        "template": "default/iq-text.iq-variant-01",
                                        "name": "placeholder",
                                        "text": "-"
                                    }
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": "iq-button iq-variant-02 date-time-selection",
                                        "template": "default/iq-button.date-time-selection",
                                        "label": "uninitialized",
                                        "_controllers_": getControllerConfig("buttonSelectEndDate")
                                    }
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false
                            },
                            "children": [
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01",
                                        "template": "default/iq-text.iq-variant-01",
                                        "name": "placeholder",
                                        "text": "${ui-action.export-alarm-history.dialog.severitySelect.label}"
                                    }
                                },
                                {
                                    "ui": "iq-checkbox",
                                    "config": {
                                        "class-name": "iq-checkbox iq-variant-01",
                                        "template": "default/iq-checkbox.iq-variant-01",
                                        "label": "${alarmlist_level_info}",
                                        "_controllers_": getControllerConfig("selectSeverityInfo")
                                    }
                                },
                                {
                                    "ui": "iq-checkbox",
                                    "config": {
                                        "class-name": "iq-checkbox iq-variant-01",
                                        "template": "default/iq-checkbox.iq-variant-01",
                                        "label": "${alarmlist_level_warning}",
                                        "_controllers_": getControllerConfig("selectSeverityWarning")
                                    }
                                },
                                {
                                    "ui": "iq-checkbox",
                                    "config": {
                                        "class-name": "iq-checkbox iq-variant-01",
                                        "template": "default/iq-checkbox.iq-variant-01",
                                        "label": "${alarmlist_level_alarm}",
                                        "_controllers_": getControllerConfig("selectSeverityAlarm")
                                    }
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container dialog-button-row",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer"
                            },
                            "children": [
                                makeButtonConfig("Cancel"),
                                makeButtonConfig("Export", ["filled"])
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * Creates a dialog prompting the user for a filter configuration.
     *
     * @returns {Promise} Promise that is resolved with the filter
     *  configuration on user confirmation or `null` on user cancellation.
     */
    module.getDialog = function getDialog() {
        return new Promise((resolve, reject) => {
            const baseElement = document.querySelector("[data-ui=container]"),
                baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

            if (!baseContainer) {
                reject(new Error("Base container element not found."));
                return;
            }

            const dialogLayout = makeDialogLayout(resolve);
            if (!dialogLayout) {
                reject(new Error("Dialog layout could not be constructed."));
                return;
            }

            baseContainer.addControl(dialogLayout, (err, controls) => {
                if (err) {
                    reject(err);
                } else {
                    controls[0].show();
                }
            });
        });
    };
})();

/**
 * Module to implement dialog used for recipe import
 *
 * @module visuals/tools/recipes/dialog
 */
(function() {
    const MODULE_NAME = "visuals.tools.recipes.dialog",
        /** @lends module:visuals/tools/recipes/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "recipe-import-export-dialog";

    const buttonTemplate = {
        "ui": "iq-button",
        "config": {
            "class-name": "iq-button iq-variant-01",
            "template": "default/iq-button.iq-variant-01",
            "action": null,
            "show-text": true,
            "disable-item-lock": false,
            "monoflop": false,
            "monoflop-value": 1,
            "monoflop-interval": 100,
            "name": "iq-button_1",
            "ui": "iq-button",
            "label": "Button 2",
            "item": null,
            "auto-label": true,
            "icon-src": null,
            "icon-title": null,
            "icon-class": null,
            "label-from-item": false,
            "write-bool": false,
            "action-release": null,
            "action-while-pressed": null,
            "interval-while-pressed": null,
            "action-press": null,
            "disable-alarms": false,
            "show-icon": false,
            "on-value": 1,
            "off-value": 0,
            "tooltip": null
        },
        "children": null
    };

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * build configuration for choice buttons
     *
     * @param {object[]} buttons button configuration for displayed choices
     * @param {string} buttons[].name name of choice, will be used as return value
     * @param {string} [buttons[].label] optional button label
     * @param {string} [buttons[].icon] optional display icon path
     * @return {object[]} iq-button configurations used for widget generation of choice buttons
     */
    function getButtonConfigs(buttons) {
        const buttonConfigs = [];
        let invalid = false;
        buttons.forEach((button) => {
            if (typeof button.name === "string" && button.name) {
                const buttonConfig = shmi.cloneObject(buttonTemplate);
                buttonConfig.config._controllers_ = getControllerConfig(`optionButton${button.name}`);
                if (typeof button.label === "string") {
                    buttonConfig.config.label = button.label;
                }
                if (typeof button.icon === "string") {
                    buttonConfig.config["icon-src"] = button.icon;
                    buttonConfig.config["show-icon"] = true;
                }
                buttonConfigs.push(buttonConfig);
            } else {
                invalid = true;
            }
        });

        return invalid ? null : buttonConfigs;
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {object[]} buttons iq-button configuration of choice buttons
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(buttons, resolve) {
        const buttonSlots = {};
        buttons.forEach((button) => {
            if (typeof button.name === "string") {
                buttonSlots[`optionButton${button.name}`] = {
                    ui: "iq-button",
                    events: ["click"],
                    optional: true
                };
            }
        });

        return {
            name: CONTROLLER_NAME,
            slots: Object.assign({
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonContainer: {
                    ui: "container",
                    optional: true
                }
            }, buttonSlots),
            onChange: function(state, detail) {},
            onEnable: function(state) {

            },
            onDisable: function(state) {

            },
            onEvent: function(state, slot, type, event) {
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                default:
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(slot.replace("optionButton", ""));
                        state.getInstance("dialog").hide();
                    }
                }
            },
            data: {
                resolved: false
            }
        };
    }

    /**
     * get configured ui layout for selection dialog
     *
     * @param {string} message dialog message text
     * @param {string} title dialog title text
     * @param {object[]} buttons choice button options
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @param {boolean} [allowClose=false] optionally allow display of close button in dialog header
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(message, title, buttons, resolve, reject, allowClose = false) {
        const buttonConfigs = getButtonConfigs(buttons);
        if (buttonConfigs === null) {
            reject(new Error("Invalid button configuration."));
            return null;
        }
        return {
            "ui": "dialog-box",
            "config": {
                "name": "messageDialog",
                "class-name": "dialog-box recipe-message-dialog" + (allowClose ? " allow-close" : ""),
                "template": "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": title,
                "content-template": null,
                "tab-limit": true,
                "show-close-button": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(buttons, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container message-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "messageContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-text",
                                    "config": {
                                        "class-name": "iq-text iq-variant-01 message-text",
                                        "template": "default/iq-text.iq-variant-01",
                                        "name": "messageText",
                                        "ui": "iq-text",
                                        "text": message,
                                        "items": []
                                    },
                                    "children": null
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container button-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer",
                                "template": null,
                                "_controllers_": getControllerConfig("buttonContainer")
                            },
                            "children": buttonConfigs
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create configurable selection dialog
     *
     * @param {string} message dialog message text
     * @param {string} title dialog title text
     * @param {object[]} buttons choice button options
     * @param {string} buttons[].name name of choice, will be used as return value
     * @param {string} [buttons[].label] optional button label
     * @param {string} [buttons[].icon] optional display icon path
     * @param {boolean} [allowClose=false] optionally allow display of close button in dialog header
     * @return {Promise<string|null>} promise resolving to choice name or `null` if closed without selection
     */
    module.getDialog = (message, title, buttons, allowClose = false) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(message, title, buttons, resolve, reject, allowClose);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].show();
                    }
                });
            }
        }
    });
}());
/**
 * Module to implement dialog used for thread creation / editing
 *
 * @module visuals/tools/threads/dialog
 */
(function() {
    const MODULE_NAME = "visuals.tools.threads.dialog",
        /** @lends module:visuals/tools/threads/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "thread-create-edit-dialog";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {boolean} create if `true` a new thread will be created, if `false` an existing thread is edited (requires `threadId` to be specified)
     * @param {object} options thread options
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(create, options, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                inputTitle: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                inputMessage: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                buttonDelete: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {
                const inputTitle = state.getInstance("inputTitle"),
                    inputMessage = state.getInstance("inputMessage"),
                    buttonDelete = state.getInstance("buttonDelete");

                if (!state.data.contentSet && inputMessage && inputTitle && buttonDelete) {
                    state.data.contentSet = true;
                    if (options.title) {
                        inputTitle.setValue(options.title);
                    }

                    if (options.message) {
                        inputMessage.setValue(options.message);
                    }

                    if (options.allowDelete !== true) {
                        shmi.addClass(buttonDelete.element, "hidden");
                    }
                }
            },
            onEnable: function(state) {

            },
            onDisable: function(state) {

            },
            onEvent: function(state, slot, type, event) {
                let messageText = null;
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonDelete":
                    state.data.resolved = true;
                    resolve({
                        "action": "delete"
                    });
                    state.getInstance("dialog").hide();
                    break;
                case "buttonApply":
                    messageText = state.getInstance("inputMessage").getValue();
                    if (typeof messageText === "string" && messageText.trim().length) {
                        state.data.resolved = true;
                        resolve({
                            "action": "apply",
                            "title": state.getInstance("inputTitle").getValue(),
                            "message": state.getInstance("inputMessage").getValue()
                        });
                        state.getInstance("dialog").hide();
                    } else {
                        shmi.notify(options.enterMessageNotification);
                    }

                    break;
                default:
                }
            },
            data: {
                resolved: false,
                contentSet: false
            }
        };
    }

    /**
     * get configured ui layout for selection dialog
     *
     * @param {boolean} create if `true` a new thread will be created, if `false` an existing thread is edited (requires `threadId` to be specified)
     * @param {object} options thread options
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.dialogTitle] dialog title text
     * @param {string} [options.titleClassName] class-name to apply to title input-field
     * @param {string} [options.titleTemplate] template to use for title input-field
     * @param {string} [options.titleLabel] label text for title input-field
     * @param {string} [options.messageClassName] class-name to apply to message input-field
     * @param {string} [options.messageTemplate] template to use for message input-field
     * @param {string} [options.messageLabel] label text for message input-field
     * @param {string} [options.deleteClassName] class-name to apply to delete button
     * @param {string} [options.deleteTemplate] template to use for delete button
     * @param {string} [options.deleteLabel] label text for delete button
     * @param {string} [options.applyClassName] class-name to apply to apply button (create/update)
     * @param {string} [options.applyTemplate] template ot use for apply button (create/update)
     * @param {string} [options.applyLabel] label text for apply button
     * @param {string} [options.enterMessageNotification] notification text displayed if no message has been entered
     * @param {boolean} [options.allowDelete] allow deletion of thread if set to `true`
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(create, options, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "threadsDialog",
                "class-name": (options.dialogClassName || "dialog-box threads-dialog") + (create ? " create" : " edit"),
                "template": options.dialogTemplate || "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": options.dialogTitle || (create ? "Create Comment" : "Edit Comment"),
                "content-template": null,
                "tab-limit": false,
                "show-close-button": true,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(create, options, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container content-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": false,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container message-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": false,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "messageContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.titleClassName || "iq-input-field iq-variant-02 title",
                                        "template": options.titleTemplate || "default/iq-input-field.iq-variant-02",
                                        "name": "commentTitle",
                                        "ui": "iq-input-field",
                                        "label": options.titleLabel || "Title",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 0,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputTitle")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.messageClassName || "iq-input-field iq-variant-02 message",
                                        "template": options.messageTemplate || "default/iq-input-field.iq-variant-02",
                                        "name": "commentMessage",
                                        "ui": "iq-input-field",
                                        "label": options.messageLabel || "Message",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 0,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": true,
                                        "notResizable": true,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputMessage")
                                    },
                                    "children": null
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container button-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.deleteClassName || "iq-button iq-variant-01 delete",
                                        "template": options.deleteTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "commentDelete",
                                        "ui": "iq-button",
                                        "label": options.deleteLabel || "Delete",
                                        "item": null,
                                        "auto-label": false,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonDelete")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.applyClassName || "iq-button iq-variant-01 apply",
                                        "template": options.applyTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "commentApply",
                                        "ui": "iq-button",
                                        "label": options.applyLabel || "Create / Update",
                                        "item": null,
                                        "auto-label": false,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonApply")
                                    },
                                    "children": null
                                }
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create thread created / edit dialog
     *
     * @param {boolean} create if `true` a new thread will be created, if `false` an existing thread is edited (requires `threadId` to be specified)
     * @param {object} options thread options
     * @return {Promise<string|null>} promise resolving to choice name or `null` if closed without selection
     */
    module.getDialog = (create, options) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(create, options, resolve, reject);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].show();
                    }
                });
            }
        }
    });
}());
/**
 * Module to implement dialog used for trend set creation / editing
 *
 * @module visuals/tools/trendExport/dialog
 */
(function() {
    const MODULE_NAME = "visuals.tools.trendExport.dialog",
        /** @lends module:visuals/tools/trendExport/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "trend-data-export-dialog";
    const defaultDateTimeFormat = "$DD.$MM.$YYYY, $HH:$mm:$ss";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    function asyncConfirm(message, title = null, param = {}) {
        return new Promise((resolve) => {
            shmi.confirm(message, resolve, title, param);
        });
    }

    function updateTimeLabels(state) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            buttonStart = state.getInstance("buttonStart"),
            buttonEnd = state.getInstance("buttonEnd");

        buttonStart.setLabel(
            formatDateTime(Math.floor(state.data.timestampStart / 1000), state.data.dateTimeFormat)
        );
        buttonEnd.setLabel(
            formatDateTime(Math.floor(state.data.timestampEnd / 1000), state.data.dateTimeFormat)
        );
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {string} trendName name of trend to export
     * @param {object} options dialog options
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(trendName, options, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                buttonStart: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonEnd: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {
                const buttonStart = state.getInstance("buttonStart"),
                    buttonEnd = state.getInstance("buttonEnd");

                if (!state.data.contentSet && buttonStart && buttonEnd) {
                    state.data.contentSet = true;
                    if (options.timestampStart) {
                        state.data.timestampStart = options.timestampStart;
                    }
                    if (options.timestampEnd) {
                        state.data.timestampEnd = options.timestampEnd;
                    }
                    if (options.dateTimeFormat) {
                        state.data.dateTimeFormat = shmi.localize(options.dateTimeFormat);
                    }
                    updateTimeLabels(state);
                }
            },
            onEnable: function(state) {

            },
            onDisable: function(state) {

            },
            onEvent: async function(state, slot, type, event) {
                let confirmed = true;
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonStart": {
                    const startTime = await shmi.visuals.tools.date.requestDateAndTime({ timestamp: state.data.timestampStart });
                    if (startTime !== null) {
                        state.data.timestampStart = startTime;
                        updateTimeLabels(state);
                    }
                    break;
                }
                case "buttonEnd": {
                    const endTime = await shmi.visuals.tools.date.requestDateAndTime({ timestamp: state.data.timestampEnd });
                    if (endTime !== null) {
                        state.data.timestampEnd = endTime;
                        updateTimeLabels(state);
                    }
                    break;
                }
                case "buttonCancel":
                    state.data.resolved = true;
                    resolve(null);
                    state.getInstance("dialog").hide();
                    break;
                case "buttonApply":
                    if (state.data.timestampEnd - state.data.timestampStart > 86400000) {
                        confirmed = await asyncConfirm("${ui-action.trend-export.confirmation.large-duration}");
                    }
                    if (confirmed) {
                        state.data.resolved = true;
                        resolve({
                            start: state.data.timestampStart,
                            end: state.data.timestampEnd
                        });
                        state.getInstance("dialog").hide();
                    }
                    break;
                default:
                }
            },
            data: {
                timestampStart: 0,
                timestampEnd: 0,
                resolved: false,
                contentSet: false,
                dateTimeFormat: defaultDateTimeFormat
            }
        };
    }

    /**
     * get configured ui layout for selection dialog
     *
     * @param {string} trendName name of trend to export
     * @param {object} options dialog options
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.dialogTitle] dialog title text
     * @param {string} [options.cancelClassName] class-name to apply to delete button
     * @param {string} [options.cancelTemplate] template to use for delete button
     * @param {string} [options.cancelLabel] label text for delete button
     * @param {string} [options.applyClassName] class-name to apply to apply button (create/update)
     * @param {string} [options.applyTemplate] template ot use for apply button (create/update)
     * @param {string} [options.applyLabel] label text for apply button
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(trendName, options, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "trendSetNameDialog",
                "class-name": options.dialogClassName || "dialog-box trend-data-export",
                "template": options.dialogTemplate || "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": options.dialogTitle || "Export Trend Data",
                "content-template": null,
                "tab-limit": false,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(trendName, options, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container content-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container selection-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "selectionContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "container",
                                    "config": {
                                        "class-name": "iq-container time-date",
                                        "type": "iqflex",
                                        "flex-orientation": "column",
                                        "flex-none": true,
                                        "flex-wrap": false,
                                        "flex-primary-align": "start",
                                        "flex-secondary-align": "stretch",
                                        "flex-line-align": "start",
                                        "name": "startTimeDate",
                                        "template": null
                                    },
                                    "children": [
                                        {
                                            "ui": "iq-label",
                                            "config": {
                                                "class-name": options.startLabelClassName || "iq-label iq-variant-01 start-label",
                                                "name": "startLabel",
                                                "ui": "iq-label",
                                                "item": null,
                                                "type": null,
                                                "auto-type": true,
                                                "decimal-delimiter": ".",
                                                "precision": 2,
                                                "auto-precision": true,
                                                "options": [],
                                                "pattern": null,
                                                "tooltip": null,
                                                "value-as-tooltip": false,
                                                "text": options.startLabel || "Start"
                                            },
                                            "children": null
                                        },
                                        {
                                            "ui": "iq-button",
                                            "config": {
                                                "class-name": options.startClassName || "iq-button iq-variant-02 date-time-selection",
                                                "template": options.startTemplate || "default/iq-button.date-time-selection",
                                                "action": null,
                                                "show-text": true,
                                                "disable-item-lock": false,
                                                "monoflop": false,
                                                "monoflop-value": 1,
                                                "monoflop-interval": 100,
                                                "name": "buttonStart",
                                                "ui": "iq-button",
                                                "label": "[Start Date/Time]",
                                                "item": null,
                                                "auto-label": true,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null,
                                                "label-from-item": false,
                                                "write-bool": false,
                                                "action-release": null,
                                                "action-while-pressed": null,
                                                "interval-while-pressed": null,
                                                "action-press": null,
                                                "disable-alarms": false,
                                                "show-icon": false,
                                                "on-value": 1,
                                                "off-value": 0,
                                                "tooltip": null,
                                                "_controllers_": getControllerConfig("buttonStart")
                                            },
                                            "children": null
                                        }
                                    ]
                                },
                                {
                                    "ui": "container",
                                    "config": {
                                        "class-name": "iq-container time-date",
                                        "type": "iqflex",
                                        "flex-orientation": "column",
                                        "flex-none": true,
                                        "flex-wrap": false,
                                        "flex-primary-align": "start",
                                        "flex-secondary-align": "stretch",
                                        "flex-line-align": "start",
                                        "name": "endTimeDate",
                                        "template": null
                                    },
                                    "children": [
                                        {
                                            "ui": "iq-label",
                                            "config": {
                                                "class-name": options.endLabelClassName || "iq-label iq-variant-01 end-label",
                                                "name": "endLabel",
                                                "ui": "iq-label",
                                                "item": null,
                                                "type": null,
                                                "auto-type": true,
                                                "decimal-delimiter": ".",
                                                "precision": 2,
                                                "auto-precision": true,
                                                "options": [],
                                                "pattern": null,
                                                "tooltip": null,
                                                "value-as-tooltip": false,
                                                "text": options.endLabel || "End"
                                            },
                                            "children": null
                                        },
                                        {
                                            "ui": "iq-button",
                                            "config": {
                                                "class-name": options.endClassName || "iq-button iq-variant-02 date-time-selection",
                                                "template": options.endTemplate || "default/iq-button.date-time-selection",
                                                "action": null,
                                                "show-text": true,
                                                "disable-item-lock": false,
                                                "monoflop": false,
                                                "monoflop-value": 1,
                                                "monoflop-interval": 100,
                                                "name": "buttonEnd",
                                                "ui": "iq-button",
                                                "label": "[End Date/Time]",
                                                "item": null,
                                                "auto-label": true,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null,
                                                "label-from-item": false,
                                                "write-bool": false,
                                                "action-release": null,
                                                "action-while-pressed": null,
                                                "interval-while-pressed": null,
                                                "action-press": null,
                                                "disable-alarms": false,
                                                "show-icon": false,
                                                "on-value": 1,
                                                "off-value": 0,
                                                "tooltip": null,
                                                "_controllers_": getControllerConfig("buttonEnd")
                                            },
                                            "children": null
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container button-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.applyClassName || "iq-button iq-variant-01 apply",
                                        "template": options.applyTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "commentApply",
                                        "ui": "iq-button",
                                        "label": options.applyLabel || "Export",
                                        "item": null,
                                        "auto-label": false,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonApply")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.cancelClassName || "iq-button iq-variant-01 cancel",
                                        "template": options.cancelTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "buttonCancel",
                                        "ui": "iq-button",
                                        "label": options.cancelLabel || "Cancel",
                                        "item": null,
                                        "auto-label": false,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonCancel")
                                    },
                                    "children": null
                                }
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create trend set create / edit dialog
     *
     * @param {string} trendName name of trend to export
     * @param {object} options dialog options
     * @param {string[]} [options.items] items to export
     * @return {Promise<string|null>} promise resolving to choice name or `null` if closed without selection
     */
    module.getDialog = (trendName, options) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(trendName, options, resolve, reject);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].show();
                    }
                });
            }
        }
    });
}());
/**
 * Module to implement dialog used for trend set creation / editing
 *
 * @module visuals/tools/trendSets/dialog
 */
(function() {
    const MODULE_NAME = "visuals.tools.trendSets.dialog",
        /** @lends module:visuals/tools/trendSets/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "trend-set-create-edit-dialog";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {boolean} create `true` for set creation, `false` for renaming
     * @param {object} setsWidget trend set selection widget
     * @param {object} options dialog options
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(create, setsWidget, options, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                inputName: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                buttonCancel: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {
                const inputName = state.getInstance("inputName"),
                    buttonCancel = state.getInstance("buttonCancel");

                if (!state.data.contentSet && inputName && buttonCancel) {
                    state.data.contentSet = true;
                    if (options.name) {
                        inputName.setValue(options.name);
                    }
                }
            },
            onEnable: function(state) {

            },
            onDisable: function(state) {

            },
            onEvent: function(state, slot, type, event) {
                let nameText = null;
                switch (slot) {
                case "dialog":
                    if (!state.data.resolved) {
                        state.data.resolved = true;
                        resolve(null);
                    }
                    shmi.deleteControl(state.getInstance("dialog"));
                    break;
                case "buttonCancel":
                    state.data.resolved = true;
                    resolve(null);
                    state.getInstance("dialog").hide();
                    break;
                case "buttonApply":
                    nameText = state.getInstance("inputName").getValue();
                    if (typeof nameText === "string" && nameText.trim().length) {
                        nameText = nameText.trim();
                        const existingNames = setsWidget.getTrendSets().map((trendSet) => trendSet.name);
                        if (existingNames.includes(nameText)) {
                            shmi.notify(options.nameExistsMessage, options.nameExistsTitle, { NAME: nameText });
                        } else {
                            state.data.resolved = true;
                            resolve({
                                "action": create ? "create" : "edit",
                                "name": state.getInstance("inputName").getValue()
                            });
                            state.getInstance("dialog").hide();
                        }
                    } else {
                        shmi.notify(options.enterNameMessage, options.enterNameTitle);
                    }

                    break;
                default:
                }
            },
            data: {
                resolved: false,
                contentSet: false
            }
        };
    }

    /**
     * get configured ui layout for selection dialog
     *
     * @param {boolean} create `true` for set creation, `false` for renaming
     * @param {object} setsWidget trend set selection widget
     * @param {object} options dialog options
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.dialogTitle] dialog title text
     * @param {string} [options.nameClassName] class-name to apply to message input-field
     * @param {string} [options.nameTemplate] template to use for message input-field
     * @param {string} [options.nameLabel] label text for message input-field
     * @param {string} [options.cancelClassName] class-name to apply to delete button
     * @param {string} [options.cancelTemplate] template to use for delete button
     * @param {string} [options.cancelLabel] label text for delete button
     * @param {string} [options.applyClassName] class-name to apply to apply button (create/update)
     * @param {string} [options.applyTemplate] template ot use for apply button (create/update)
     * @param {string} [options.applyLabel] label text for apply button
     * @param {string} [options.enterNameMessage] notification text displayed if no name has been entered
     * @param {string} [options.enterNameTitle] notification title displayed if no name has been entered
     * @param {string} [options.nameExistsMessage] notification text displayed if name already exists
     * @param {string} [options.nameExistsTitle] notification title displayed if name already exists
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(create, setsWidget, options, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "trendSetNameDialog",
                "class-name": options.dialogClassName || ("dialog-box trend-sets-dialog" + (create ? " create" : " edit")),
                "template": options.dialogTemplate || "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": options.dialogTitle || (create ? "Create Trend Set" : "Edit Trend Set"),
                "content-template": null,
                "tab-limit": false,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(create, setsWidget, options, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container content-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container message-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "messageContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.nameClassName || "iq-input-field iq-variant-02 name",
                                        "template": options.nameTemplate || "default/iq-input-field.iq-variant-02",
                                        "name": "inputName",
                                        "ui": "iq-input-field",
                                        "label": options.nameLabel || "Trend Set Name",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 0,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputName")
                                    },
                                    "children": null
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container button-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": true,
                                "flex-primary-align": "end",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.applyClassName || "iq-button iq-variant-01 apply",
                                        "template": options.applyTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "commentApply",
                                        "ui": "iq-button",
                                        "label": options.applyLabel || "Create",
                                        "item": null,
                                        "auto-label": false,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonApply")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.cancelClassName || "iq-button iq-variant-01 cancel",
                                        "template": options.cancelTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "buttonCancel",
                                        "ui": "iq-button",
                                        "label": options.cancelLabel || "Cancel",
                                        "item": null,
                                        "auto-label": false,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonCancel")
                                    },
                                    "children": null
                                }
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create trend set create / edit dialog
     *
     * @param {boolean} create `true` for set creation, `false` for renaming
     * @param {object} setsWidget trend set selection widget
     * @param {object} options dialog options
     * @return {Promise<string|null>} promise resolving to choice name or `null` if closed without selection
     */
    module.getDialog = (create, setsWidget, options) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(create, setsWidget, options, resolve, reject);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].show();
                    }
                });
            }
        }
    });
}());
/**
 * UI-Action 'set-color-mode'.
 *
 * Description:
 * Select color variable set of currently active theme.
 */
(function() {
    const actions = shmi.pkg("visuals.session.userActions"); //get reference to userActions object

    /**
     * UI-Action 'set-color-mode' implementation
     *
     * @params {any[]} parameters  configured ui-action parameters
     * @param {string} parameters[0]  Color Mode
     *
     */
    actions["set-color-mode"] = function(parameters) {
        shmi.requires("visuals.tools.theming").setColorMode(parameters[0]);
    };
}());
/**
 * UI-Action 'set-color-theme'.
 *
 * Description:
 * set active color theme
 */
(function() {
    const actions = shmi.pkg("visuals.session.userActions"); //get reference to userActions object

    /**
     * UI-Action 'set-color-theme' implementation
     *
     * @params {any[]} parameters  configured ui-action parameters
     * @param {string} parameters[0]  Theme Name
     *
     */
    actions["set-color-theme"] = function(parameters) {
        shmi.requires("visuals.tools.theming").setTheme(parameters[0]);
    };
}());