facefull.js

/////////////////////////////////////////////////////////////////////////////
// Name:        facefull.js
// Purpose:     Main Facefull module
// Author:      Nickolay Babbysh
// Version:     0.9.9
// Copyright:   (c) NickWare Group
// Licence:     MIT
/////////////////////////////////////////////////////////////////////////////

/*===================== General =====================*/

let facefull = null;

function bind(func, context) {
    return function() {
        return func.apply(context, arguments);
    };
}

String.prototype.replaceAt = function(index, replacement) {
    return this.substr(0, index) + replacement + this.substr(index+replacement.length);
}

function fixEvent(e) {
    e.currentTarget = this;
    e.target = e.srcElement;
    if (e.type === 'mouseover' || e.type === 'mouseenter') e.relatedTarget = e.fromElement;
    if (e.type === 'mouseout' || e.type === 'mouseleave') e.relatedTarget = e.toElement;
    if (e.pageX == null && e.clientX != null) {
        let html = document.documentElement;
        let body = document.body;
        e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0);
        e.pageX -= html.clientLeft || 0;
        e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0);
        e.pageY -= html.clientTop || 0;
    }
    if (!e.which && e.button) {
        e.which = e.button & 1 ? 1 : (e.button & 2 ? 3 : (e.button & 4 ? 2 : 0));
    }
    return e;
}

/**
 * Create main class.
 * @param native Set to true if you want to initialize window caption and control buttons for native application.
 */
function facefullCreate(native = false) {
    facefull = new Facefull(native);
}

/**
 * Main facefull class.
 * @param native
 * @constructor
 */
function Facefull(native = false) {
    /**
     * Subpages associative array. Use data-subpagename HTML tag to set element name.
     * @type {array}
     */
    this.Subpages = [];
    /**
     * Scrollboxes associative array. Use data-scrollboxname HTML tag to set element name.
     * @type {array}
     */
    this.Scrollboxes = [];
    /**
     * Comboboxes associative array. Use data-comboboxname HTML tag to set element name.
     * @type {array}
     */
    this.Comboboxes = [];
    /**
     * Lists associative array. Use data-listname HTML tag to set element name.
     * @type {array}
     */
    this.Lists = [];
    /**
     * Tooltips associative array.
     * @type {array}
     */
    this.Tooltips = [];
    /**
     * Popup menus associative array. Use data-popupmenu HTML tag to set element name.
     * @type {array}
     */
    this.PopupMenus = [];
    /**
     * Drop areas associative array. Use data-dropname HTML tag to set element name.
     * @type {array}
     */
    this.DropAreas = [];
    /**
     * Tabs associative array. Use data-tabsname HTML tag to set element name.
     * @type {array}
     */
    this.Tabs = [];
    /**
     * Circlebars associative array. Use data-circlebarname HTML tag to set element name.
     * @type {array}
     */
    this.Circlebars = [];
    /**
     * Counters associative array. Use data-countername HTML tag to set element name.
     * @type {array}
     */
    this.Counters = [];
    /**
     * Hotkey holders associative array. Use data-hotkeyholdername HTML tag to set element name.
     * @type {array}
     */
    this.HotkeyHolders = [];
    /**
     * Item pickers associative array. Use data-itempickername HTML tag to set element name.
     * @type {array}
     */
    this.ItemPickers = [];
    this.LastGlobalOpenedPopupMenu = null;
    this.LastGlobalOpenedPopupMenuTarget = null;
    this.Subpagelevel = 0;
    /**
     * Main menu object.
     * @type {null}
     */
    this.MainMenuBox = null;
    this.OverlayZIndex = 200;
    this.EventTable = [];
    this.Themes = null;
    this.Viewports = null;
    this.Locales = null;
    this.native = native;

    /**
     * Attaches an event handler to the bridge event name.
     * @param comm
     * @param handler
     */
    this.doEventHandlerAttach = function(comm, handler = function(data = ""){}) {
        this.EventTable[comm] = {handler: handler};
    }

    /**
     * Handles an incoming bridge event.
     * @param comm
     * @param data
     */
    this.doEventHandle = function(comm, data) {
        if (this.EventTable[comm] !== undefined && this.EventTable[comm] !== null) {
            try {
                this.EventTable[comm].handler(data);
            } catch (err) {
                console.error(err.stack);
            }
        }
    }

    /**
     * Sends event to the bridge. Uses title tag to pass event name and data across the bridge.
     * @param comm
     * @param data
     */
    this.doEventSend = function(comm, data = "") {
        document.title = "0";
        document.title = comm+"|"+data;
    }

    /**
     * Sends event to the bridge. Uses 'facefullio' script message handler to pass event name and data across the bridge.
     * @param comm
     * @param data
     */
    this.doEventSendEx = function(comm, data = "") {
        window.facefullio.postMessage(comm+"|"+data)
    }

    /**
     * Returns HEX color string from predefined color circular list.
     * @param id
     */
    this.getColorFromGrid = function(id) {
        let colors = [
            '#FF4A0C',
            '#be6e00',
            '#5D9D32',
            '#3A9470',
            '#BF1332',
            '#951656',
            '#5E1B92',
        ];
        return colors[id-Math.floor(id/colors.length)*colors.length];
    }

    /**
     * Loads CSS by filename.
     * @param file
     */
    this.doCSSLoad = function(file) {
        let link = document.createElement("link");
        link.setAttribute("rel", "stylesheet");
        link.setAttribute("type", "text/css");
        link.setAttribute("href", file);
        document.getElementsByTagName("head")[0].appendChild(link);
    }

    /**
     * Unloads CSS by filename.
     * @param file
     */
    this.doCSSUnload = function(file) {
        let ehead = document.getElementsByTagName("head")[0];
        for (let i = 0; i < ehead.childElementCount; i++) {
            let erule = ehead.children[i];
            if (erule.getAttribute("href") === file) ehead.removeChild(erule);
        }
    }

    /**
     * Decodes HES string to UTF-8 text.
     * @param hexdata
     * @returns {string}
     */
    this.doHex2String = function(hexdata) {
        let hexstr = hexdata.toString();
        let str = "";
        for (let i = 0; i < hexstr.length; i += 4) {
            str += String.fromCharCode(parseInt(hexstr.substr(i, 4), 16));
        }
        return str;
    }

    /**
     * Close all opened subpages.
     */
    this.doCloseAllSubpages = function() {
        for (let i in this.Subpages) {
            if (this.Subpages.hasOwnProperty(i))
                this.Subpages[i].doSubpageClose();
        }
    }

    this.doCloseAllPopup = function(event) {
        event = event || fixEvent.call(this, window.event);
        if (!event || (!event.target.classList.contains("Opened") && !event.target.parentElement.classList.contains("Opened") && !event.target.parentElement.parentElement.classList.contains("Opened"))) {
            for (let i in this.Comboboxes) {
                if (this.Comboboxes.hasOwnProperty(i))
                    this.Comboboxes[i].doCloseComboboxList();
            }
        }
        if (this.LastGlobalOpenedPopupMenu) {
            if (this.LastGlobalOpenedPopupMenu.epm.contains(event.target) ||
                this.LastGlobalOpenedPopupMenu.epmtarget.contains(event.target) ) return;
        }
        this.doCloseGlobalPopupMenu();
    }

    /**
     * Close all popup menus.
     */
    this.doCloseGlobalPopupMenu = function() {
        if (this.LastGlobalOpenedPopupMenu) this.LastGlobalOpenedPopupMenu.doClosePopupMenu();
    }

    /**
     * Uodate scrollbars on all scrollboxes.
     */
    this.doUpdateAllScrollboxes = function() {
        for (let i in this.Scrollboxes) {
            if (this.Scrollboxes.hasOwnProperty(i))
                this.Scrollboxes[i].doUpdateScrollbar();
        }
    }

    this.doWindowHeaderInit = function() {
        let ewcaption = document.getElementsByClassName("WindowCaption");
        let ewmover = document.getElementsByClassName("WindowMover");
        let ewctrlmin = document.getElementsByClassName("WindowControl Min");
        let ewctrlmax = document.getElementsByClassName("WindowControl Max");
        let ewctrlclose = document.getElementsByClassName("WindowControl Close");
        if (ewcaption.length) ewcaption[0].onmousedown = bind(function() {
            facefull.doEventSend("doWindowMove");
        }, this);
        if (ewmover.length) ewmover[0].onmousedown = bind(function() {
            facefull.doEventSend("doWindowMove");
        }, this);
        if (ewctrlmin.length) ewctrlmin[0].onclick = bind(function() {
            facefull.doEventSend("doWindowMinimize");
        }, this);
        if (ewctrlmax.length) ewctrlmax[0].onclick = bind(function() {
            if (ewctrlmax[0].classList.contains("Restore")) ewctrlmax[0].classList.remove("Restore");
            else ewctrlmax[0].classList.add("Restore");
            facefull.doEventSend("doWindowMaximize");
        }, this);
        if (ewctrlclose.length) ewctrlclose[0].onclick = bind(function() {
            facefull.doEventSend("doWindowClose");
        }, this);
    }

    /**
     * Starts Facefull initialization.
     * @param disableContextmenu
     */
    this.doInit = function(disableContextmenu = false) {
        let subpages = document.querySelectorAll(".Subpage");
        for (let i = 0; i < subpages.length; i++) {
            let did = subpages[i].getAttribute("data-subpagename");
            this.Subpages[did] = new Subpage(subpages[i]);
        }

        let sboxes = document.querySelectorAll(".Box.Scrolling");
        for (let i = 0; i < sboxes.length; i++) {
            let did = sboxes[i].getAttribute("data-scrollboxname");
            this.Scrollboxes[did] = new Scrollbox(sboxes[i]);
        }

        let comboxes = document.querySelectorAll(".Combobox");
        for (let i = 0; i < comboxes.length; i++) {
            let did = comboxes[i].getAttribute("data-comboboxname");
            this.Comboboxes[did] = new Combobox(comboxes[i]);
        }

        let lists = document.querySelectorAll(".List");
        for (let i = 0; i < lists.length; i++) {
            let did = lists[i].getAttribute("data-listname");
            this.Lists[did] = new List(lists[i]);
        }

        this.MainMenuBox = new MainMenu(document.querySelectorAll(".MainMenuItems").item(0));

        let tooltips = document.querySelectorAll(".TooltipTarget");
        for (let i = 0; i < tooltips.length; i++) this.Tooltips.push(new Tooltip(tooltips[i]));

        let popupmenus = document.querySelectorAll(".PopupMenuTarget");
        for (let i = 0; i < popupmenus.length; i++) {
            let did = popupmenus[i].getAttribute("data-popupmenu");
            this.PopupMenus[did] = new PopupMenu(popupmenus[i]);
        }

        let drops = document.querySelectorAll(".DropArea");
        for (let i = 0; i < drops.length; i++) {
            let did = drops[i].getAttribute("data-dropname");
            this.DropAreas[did] = new DropArea(drops[i]);
        }

        let tabs = document.querySelectorAll(".Tabs");
        for (let i = 0; i < tabs.length; i++) {
            let did = tabs[i].getAttribute("data-tabsname");
            this.Tabs[did] = new Tabs(tabs[i]);
        }

        let circles = document.querySelectorAll(".Circlebar");
        for (let i = 0; i < circles.length; i++) {
            let did = circles[i].getAttribute("data-circlebarname");
            this.Circlebars[did] = new Circlebar(circles[i]);
        }

        let counters = document.querySelectorAll(".Counter");
        for (let i = 0; i < counters.length; i++) {
            let did = counters[i].getAttribute("data-countername");
            this.Counters[did] = new Counter(counters[i]);
        }

        let hotkeyholders = document.querySelectorAll(".HotkeyHolder");
        for (let i = 0; i < hotkeyholders.length; i++) {
            let did = hotkeyholders[i].getAttribute("data-hotkeyholdername");
            this.HotkeyHolders[did] = new HotkeyHolder(hotkeyholders[i]);
        }

        let itempickers = document.querySelectorAll(".ItemPicker");
        for (let i = 0; i < itempickers.length; i++) {
            let did = itempickers[i].getAttribute("data-itempickername");
            this.ItemPickers[did] = new List(itempickers[i], "picker");
        }

        window.addEventListener("mousedown", bind(function(event) {
            this.doCloseAllPopup(event);
        }, this));
        window.addEventListener("resize", bind(function() {
            this.doUpdateAllScrollboxes();
        }, this));

        /**
         * Theme management system that provides the easiest way to control application styles.
         * @type {ThemeManager}
         */
        this.Themes = new ThemeManager();

        /**
         * Viewport manager system that provides control display modes on different devices.
         * @type {ViewportManager}
         */
        this.Viewports = new ViewportManager();

        this.Locales = new LocaleManager();

        if (native) {
            if (disableContextmenu) {
                document.addEventListener('contextmenu', function (event) {
                    event.preventDefault();
                    return false;
                }, false);
            }
            this.doWindowHeaderInit();
        }
    }
}

/*===================== ThemeManager =====================*/

/**
 * Theme manager class.
 * @constructor
 */
function ThemeManager() {
    this.table = [];
    this.current = 0;
    this.onThemeApply = function(id){}

    /**
     * Adds a theme with the given CSS filename and name to the theme table. Default theme will be added automatically during initialization with index 0.
     * @param themename
     * @param filenames
     */
    this.doAttachThemeFile = function(themename, filenames = []) {
        this.table.push({themename: themename, filenames: filenames});
    }

    /**
     * Apply theme with specified id.
     * @param id
     */
    this.doApplyTheme = function(id) {
        if (this.current === id || id < 0) return;
        if (this.current) {
            this.table[this.current].filenames.forEach(filename => {
                facefull.doCSSUnload(filename);
            });
        }
        this.current = id;
        if (id) {
            this.table[id].filenames.forEach(filename => {
                facefull.doCSSLoad(filename);
            });
        }
        this.onThemeApply(id);
    }

    /**
     * Sets the name of default theme.
     * @param name
     */
    this.setDefaultThemeName = function(name) {
        this.table[0] = {themename: name, filename: ""};
    }

    /**
     * Get last applied theme id.
     * @returns {number|*}
     */
    this.getCurrentThemeID = function() {
        return this.current;
    }

    /**
     * Get contents of theme table.
     * @returns {array}
     */
    this.getThemeList = function() {
        return this.table;
    }

    this.doAttachThemeFile("Original", "");
}

/*===================== ViewportManager =====================*/

/**
 * Viewport manager class.
 * @constructor
 */
function ViewportManager() {
    this.ruletable = [];
    this.devdeftable = [];

    this.isRuleActive = function(devdef) {
        let activeflag = true;
        if (devdef.width !== "none") {
            activeflag &= window.innerWidth <= devdef.width;
        }
        if (devdef.height !== "none") {
            activeflag &= window.innerHeight <= devdef.height;
        }
        return activeflag;
    }

    /**
     * Adds new device definition with specified name. An execution rule contains information about the conditions under which it will be executed. You can set a specific screen width and height, as well as the operating system of the device.
     * @param name
     * @param width
     * @param height
     * @param os
     */
    this.doAddDeviceDefinition = function(name, width, height = "none", os = "none") {
        this.devdeftable[name] = {width: width, height: height, os: os};
    }

    /**
     * Allows you to associate a device definition with a callback function
     * @param devdefname
     * @param rulecallback
     */
    this.doAddRule = function(devdefname, rulecallback) {
        this.ruletable.push({devdefname: devdefname, action: rulecallback});
    }

    /**
     * Starts condition processing and calls appropriate handlers.
     */
    this.doProcessRules = function() {
        this.ruletable.forEach(rule => {
            let devdef = this.devdeftable[rule.devdefname];
            if (devdef) {
                rule.action(this.isRuleActive(devdef));
            }
        });
    }
}

/*===================== LocaleManager =====================*/

/**
 * Locale manager class.
 * @constructor
 */
function LocaleManager() {
    this.table = [];
    this.dictionary = [];
    this.current = -1;
    this.onLocaleApply = function(id){}

    /**
     * Adds a locale file with the given CSS filename and name to the locale table.
     * @param localename
     * @param filenames
     */
    this.doAttachLocaleFile = function(localename, filenames = []) {
        this.table.push({localename: localename, filenames: filenames});
        this.dictionary[localename] = [];
    }

    /**
     * Adds localized string to dictionary with specified localename and  id.
     * @param localename
     * @param id
     * @param value
     */
    this.doAddToDictionary = function(localename, id, value) {
        this.dictionary[localename][id] = value;
    }

    /**
     * Apply locale with specified id.
     * @param id
     */
    this.doApplyLocale = function(id) {
        if (this.current === id || id < 0) return;
        if (this.current >= 0) {
            this.table[this.current].filenames.forEach(filename => {
                facefull.doCSSUnload(filename);
            });
        }
        this.current = id;
        this.table[id].filenames.forEach(filename => {
            facefull.doCSSLoad(filename);
        });
        this.onLocaleApply(id);
    }

    /**
     * Get value from dictionary by id for current locale.
     * @param id
     * @returns {*}
     */
    this.getValueFromDictionary = function(id) {
        return this.dictionary[this.table[this.current].localename][id];
    }

    /**
     * Get id of current locale.
     * @returns {number|*}
     */
    this.getCurrentLocaleID = function() {
        return this.current;
    }

    /**
     * Get list of locales.
     * @returns {array}
     */
    this.getLocaleList = function() {
        return this.table;
    }
}

/*===================== Subpage =====================*/

/**
 * Subpage UI element class. Use data-subpagename HTML tag to set element name.
 * @param e
 * @constructor
 */
function Subpage(e) {
    this.esubpage = e;
    this.ebackbutton = this.esubpage.children[0].children[0];
    this.opened = false;

    /**
     * Closes the subpage.
     */
    this.doSubpageClose = function() {
        if (!this.opened) return;
        this.esubpage.classList.remove("Show");
        if (facefull.Subpagelevel > 0) facefull.Subpagelevel--;
        this.opened = false;
    };

    /**
     * Opens the subpage.
     */
    this.doSubpageOpen = function() {
        if (this.opened) return;
        this.esubpage.classList.add("Show");
        this.esubpage.style.zIndex = (facefull.Subpagelevel+1)*10;
        facefull.Subpagelevel++;
        this.opened = true;
    };

    /**
     * Checks if subpage opened.
     * @returns {boolean|*}
     */
    this.isOpened = function() {
        return this.opened;
    };

    let SPL = document.querySelectorAll("[data-subpageopen='"+this.esubpage.getAttribute("data-subpagename")+"']");
    for (let i = 0; i < SPL.length; i++) SPL[i].onclick = bind(this.doSubpageOpen, this);
    this.ebackbutton.onclick = bind(this.doSubpageClose, this);
}

/*===================== Scrollbox =====================*/

/**
 * Scrollbox UI element class. Use data-scrollboxname HTML tag to set element name.
 * @param e
 * @constructor
 */
function Scrollbox(e) {
    this.escrollbox = e;
    this.escrolldata = e.children[0];
    this.lasttouchshift = 0;

    this.doCreateScrollbar = function() {
        this.escrollbarblock = document.createElement("div");
        this.escrollbartrack = document.createElement("div");
        this.escrollbarblock.className = "Scrollbar-block";
        this.escrollbartrack.className = "Scrollbar-track";
        this.escrollbox.appendChild(this.escrollbarblock);
        this.escrollbarblock.appendChild(this.escrollbartrack);
        this.escrollbartrack.style.height = this.escrollbox.offsetHeight * this.escrollbox.offsetHeight / this.escrolldata.offsetHeight + "px";
        this.escrollbartrack.ondragstart = function() {
            return false;
        };
        this.escrollbartrack.addEventListener("mousedown", bind(this.onStartMoveScrollbarTrack, this));
        this.escrollbarblock.addEventListener("touchstart", bind(this.onTouchStartScrollbar, this));
        this.escrollbarblock.addEventListener("touchmove", bind(this.onTouchMoveScrollbar, this));
    };

    this.doRemoveScrollbar = function() {
        if (this.escrollbarblock !== undefined) {
            this.escrollbox.removeChild(this.escrollbarblock);
            this.escrolldata.style.marginTop = "0px";
            delete this.escrollbartrack;
            delete this.escrollbarblock;
        }
    };

    /**
     * Update scrollbar on this scrollbox.
     */
    this.doUpdateScrollbar = function() {
        if (this.escrollbox.offsetHeight < this.escrolldata.offsetHeight) {
            if (this.escrollbarblock !== undefined) {
                this.escrollbartrack.style.height = this.escrollbox.offsetHeight * this.escrollbox.offsetHeight / this.escrolldata.offsetHeight + "px";
                if (this.escrollbartrack.offsetTop+this.escrollbartrack.offsetHeight > this.escrollbox.offsetHeight)
                    this.escrollbartrack.style.marginTop = this.escrollbox.offsetHeight - this.escrollbartrack.offsetHeight + "px";
                this.escrolldata.style.marginTop = - this.escrollbartrack.offsetTop * (this.escrolldata.offsetHeight-this.escrollbox.offsetHeight)/(this.escrollbox.offsetHeight-this.escrollbartrack.offsetHeight) + "px";
            } else this.doCreateScrollbar();
        } else this.doRemoveScrollbar();
    };

    this.onStartMoveScrollbarTrack = function(event) {
        event = event || fixEvent.call(this, window.event);
        this.scrollbartrackoffset = event.clientY - this.escrollbartrack.offsetTop;
        document.onmousemove = bind(this.onMoveScrollbarTrack, this);
        document.onmouseup = bind(this.onEndMoveScrollbarTrack, this);
    };

    this.onMoveScrollbarTrack = function(event) {
        event = event || fixEvent.call(this, window.event);
        this.doMoveScrollbar(event.clientY-this.scrollbartrackoffset);
    };

    this.onEndMoveScrollbarTrack = function() {
        document.onmousemove = null;
        document.onmouseup = null;
    };

    this.onWheelScrollbar = function(event) {
        facefull.doCloseGlobalPopupMenu();
        let d = 60;
        event = event || window.event;
        let ep = event.target;
        let hasnested = false;
        while (ep !== undefined && ep !== null && ep !== this.escrollbox) {
            if (ep.classList.contains("Scrolling")) hasnested = true;
            ep = ep.parentElement;
        }
        if (hasnested) return;
        if ((event.deltaY || event.detail || event.wheelDelta) < 0) d = -d;
        let delta = 0;
        if (this.escrollbartrack !== undefined) {
            delta = d * this.escrollbartrack.offsetHeight / this.escrollbox.offsetHeight;
            this.doMoveScrollbar(this.escrollbartrack.offsetTop+delta);
        } else this.doMoveScrollbar(0);
    };

    this.doMoveScrollbar = function(pos) {
        if (this.escrollbartrack === undefined) {
            this.escrolldata.style.marginTop = "0";
            return;
        }
        this.escrollbartrack.style.marginTop = pos + "px";
        if (this.escrollbartrack.offsetTop+this.escrollbartrack.offsetHeight > this.escrollbox.offsetHeight)
            this.escrollbartrack.style.marginTop = this.escrollbox.offsetHeight - this.escrollbartrack.offsetHeight + "px";
        else if (this.escrollbartrack.offsetTop < 0)
            this.escrollbartrack.style.marginTop = 0 + "px";
        this.escrolldata.style.marginTop = -this.escrollbartrack.offsetTop * (this.escrolldata.offsetHeight-this.escrollbox.offsetHeight) / (this.escrollbox.offsetHeight-this.escrollbartrack.offsetHeight) + "px";
    };

    this.doMoveScrolldata = function(pos) {
        if (this.escrollbartrack === undefined) {
            this.escrolldata.style.marginTop = "0";
            return;
        }
        this.escrolldata.style.marginTop = pos + "px";
        if (this.escrolldata.offsetHeight+this.escrolldata.offsetTop < this.escrollbox.offsetHeight)
            this.escrolldata.style.marginTop = -(this.escrolldata.offsetHeight-this.escrollbox.offsetHeight) + "px";
        else if (this.escrolldata.offsetTop > 0)
            this.escrolldata.style.marginTop = 0 + "px";
        this.escrollbartrack.style.marginTop = -this.escrolldata.offsetTop / (this.escrolldata.offsetHeight-this.escrollbox.offsetHeight) * (this.escrollbox.offsetHeight-this.escrollbartrack.offsetHeight) + "px";
    };

    this.onTouchStartScrollbar = function(event) {
        let touches = event.changedTouches;
        if (touches.length >= 0) {
            this.lasttouchshift = touches[0].pageY;
        }
    };

    this.onTouchMoveScrollbar = function(event) {
        let touches = event.changedTouches;
        if (touches.length >= 0) {
            if (this.escrollbartrack !== undefined) {
                let delta = touches[0].pageY - this.lasttouchshift;
                this.lasttouchshift = touches[0].pageY;
                this.doMoveScrollbar(this.escrollbartrack.offsetTop+delta);
            } else this.doMoveScrollbar(0);
        }
    };

    this.onTouchStartScrollbox = function(event) {
        let touches = event.changedTouches;
        if (touches.length >= 0) {
            this.lasttouchshift = touches[0].pageY;
        }
    };

    this.onTouchMoveScrollbox = function(event) {
        let touches = event.changedTouches;
        if (touches.length >= 0) {
            let ep = event.target;
            let hasnested = false;
            while (ep !== undefined && ep !== null && ep !== this.escrollbox) {
                if (ep.classList.contains("Scrolling")) hasnested = true;
                ep = ep.parentElement;
            }
            if (hasnested) return;
            if (this.escrollbartrack !== undefined) {
                let delta = touches[0].pageY - this.lasttouchshift;
                this.lasttouchshift = touches[0].pageY;
                this.doMoveScrolldata(this.escrolldata.offsetTop+delta);
            } else this.doMoveScrolldata(0);
        }
    };

    /**
     * Sets srollbar to end position.
     */
    this.doScrollToEnd = function() {
        this.escrollbartrack.style.marginTop = this.escrollbox.offsetHeight - this.escrollbartrack.offsetHeight + "px";
        this.escrolldata.style.marginTop = -this.escrollbartrack.offsetTop * (this.escrolldata.offsetHeight-this.escrollbox.offsetHeight)/(this.escrollbox.offsetHeight-this.escrollbartrack.offsetHeight) + "px";
    }

    /**
     * Sets scrollbar to specified position.
     * @param position
     */
    this.setScrollPosition = function(position) {
        let dx = 0;
        if (this.escrollbartrack !== undefined)
            dx = this.escrollbartrack.offsetHeight / this.escrollbox.offsetHeight;
        this.doMoveScrollbar(position*dx);
    };

    this.setScrollEnd = function() {
        this.doMoveScrollbar(this.escrollbox.offsetHeight);
    }

    /**
     * Checks if scrollbar is at its end position.
     * @returns {boolean}
     */
    this.isScrollOnEnd = function() {
        if (this.escrollbartrack === undefined) return true;
        return this.escrollbartrack.offsetTop+this.escrollbartrack.offsetHeight === this.escrollbox.offsetHeight;
    }

    /**
     * Get scrollbox object.
     * @returns {*}
     */
    this.getScrollbox = function () {
        return this.escrollbox;
    };

    this.escrollbox.addEventListener("wheel", bind(this.onWheelScrollbar, this));
    this.escrollbox.addEventListener("mousewheel", bind(this.onWheelScrollbar, this));
    this.escrollbox.addEventListener("touchstart", bind(this.onTouchStartScrollbox, this));
    this.escrollbox.addEventListener("touchmove", bind(this.onTouchMoveScrollbox, this));
    if (this.escrollbox.offsetHeight < this.escrolldata.offsetHeight) this.doCreateScrollbar();
    this.escrolldata.style.marginTop = "0px";
}

/*===================== Combobox =====================*/

/**
 * Combobox UI element class. Use data-comboboxname HTML tag to set element name.
 * @param e
 * @constructor
 */
function Combobox(e) {
    this.ecombobox = e;
    this.ecomboboxtitle = e.children[0].children[0];
    this.ecomboboxdata = e.children[1];
    this.state = 0;
    this.onChangeState = function(state){};

    this.doOpenComboboxList = function() {
        this.ecomboboxdata.style.display = "block";
        this.ecombobox.classList.add("Opened");
    };

    this.doCloseComboboxList = function() {
        this.ecomboboxdata.style.display = "none";
        this.ecombobox.classList.remove("Opened");
    };

    /**
     * Sets combobox default title.
     * @param title
     * @param caption
     */
    this.doSetComboboxTitle = function(title, caption) {
        this.ecomboboxtitle.innerHTML = title;
        this.ecomboboxtitle.setAttribute("data-caption", caption);
    };

    this.doChangeState = function(event) {
        event = event || fixEvent.call(this, window.event);
        if (event.target.classList.contains("Disabled")) return;
        if (this.ecomboboxdata.style.display === "block") this.doCloseComboboxList();
        else this.doOpenComboboxList();
        if (event.target.tagName === "LI") {
            for (let i = 0; i < this.ecomboboxdata.childElementCount; i++) {
                if (this.ecomboboxdata.children[i] === event.target) {
                    this.state = i;
                    this.onChangeState(this.state);
                    break;
                }
            }
            this.doSetComboboxTitle(event.target.innerHTML, event.target.getAttribute("data-caption"));
        }
    };

    /**
     * Choose specified combobox element.
     * @param state
     */
    this.setState = function(state) {
        this.state = state;
        this.onChangeState(this.state);
        this.doSetComboboxTitle(this.ecomboboxdata.children[state].innerHTML, this.ecomboboxdata.children[state].getAttribute("data-caption"));
    };

    /**
     * Get current combobox element.
     * @returns {number|*}
     */
    this.getState = function() {
        return this.state;
    };

    /**
     * Add item to the combobox list.
     * @param title
     * @param caption
     */
    this.doAddItem = function(title, caption = "") {
        let item = document.createElement("li");
        item.innerHTML = title;
        item.setAttribute("data-caption", caption);
        this.ecomboboxdata.appendChild(item);
    };

    /**
     * Clear the combobox list.
     */
    this.doClear = function() {
        this.ecomboboxtitle.innerHTML = "";
        this.ecomboboxdata.innerHTML = "";
    };

    e.onclick = bind(this.doChangeState, this);
}

/*===================== MainMenu =====================*/

/**
 * Main menu UI element class.
 * @param e
 * @constructor
 */
function MainMenu(e) {
    this.emainmenu = e;
    this.currentmenuitem = this.emainmenu.children[0];
    this.currentmenuitem.classList.add("Active");
    this.currentpage = document.getElementById("P"+this.currentmenuitem.getAttribute("data-pagename"));
    this.currentpage.classList.add("Show");
    this.onPageOpen = function(name) {}

    this.doPageOpen = function(e) {
        this.currentpage.classList.remove("Show");
        this.currentmenuitem.classList.remove("Active");
        this.currentmenuitem = e;
        this.currentmenuitem.classList.add("Active");
        let pname = this.currentmenuitem.getAttribute("data-pagename");
        this.currentpage = document.getElementById("P"+pname);
        this.currentpage.classList.add("Show");
        this.onPageOpen(pname);
    };

    this.doPageOpenByEvent = function(event) {
        this.doPageOpen(event.target);
    }

    /**
     * Open page specified by name.
     * @param pname
     */
    this.doPageOpenByName = function(pname) {
        for (let i = 0; i < this.emainmenu.children.length; i++)
            if (this.emainmenu.children[i].getAttribute("data-pagename").toLowerCase() === pname) this.doPageOpen(this.emainmenu.children[i]);
    }

    /**
     * Checks if the page is open.
     * @param id
     * @returns {boolean}
     */
    this.isPageOpened = function(id) {
        return this.currentpage.id === "P"+id;
    }

    this.getElement = function() {
        return this.emainmenu;
    }

    for (let i = 0; i < this.emainmenu.children.length; i++) this.emainmenu.children[i].onclick = bind(this.doPageOpenByEvent, this);
}

/*===================== Alert =====================*/

/**
 * Show an alert window.
 * @param caption
 * @param text
 * @param type
 * @param buttons
 * @param callbacks
 * @param captionlid
 * @param textlid
 * @constructor
 */
function AlertShow(caption, text, type = "info", buttons = "OK", callbacks = [], captionlid = "", textlid = "") {
    document.getElementById("AE").classList.remove("Warning");
    document.getElementById("AE").classList.remove("Error");
    facefull.OverlayZIndex += 5;
    switch (type) {
        case "info":
            break;
        case "warning":
            document.getElementById("AE").classList.add("Warning");
            break;
        case "error":
            document.getElementById("AE").classList.add("Error");
            break;
    }

    let eabok = document.getElementById("AB-OK");
    let eaby = document.getElementById("AB-Y");
    let eabn = document.getElementById("AB-N");
    eabok.style.display = "none";
    eaby.style.display = "none";
    eabn.style.display = "none";

    switch (buttons) {
        case "OK":
            eabok.style.display = "block";
            eabok.onclick = function() {
                AlertHideCustom('AE');
                if (callbacks.length > 0) callbacks[0]();
            }
            break;
        case "YESNO":
            eaby.style.display = "block";
            eabn.style.display = "block";
            eaby.onclick = function() {
                AlertHideCustom('AE');
                if (callbacks.length > 0) callbacks[0]();
            }
            eabn.onclick = function() {
                AlertHideCustom('AE');
                if (callbacks.length > 1) callbacks[1]();
            }
            break;
    }

    document.getElementById("AE").children[0].innerHTML = caption;
    document.getElementById("AE").children[0].setAttribute("data-caption", captionlid);
    document.getElementById("AE").children[1].innerHTML = text;
    document.getElementById("AE").children[1].setAttribute("data-caption", textlid);
    document.getElementById("OV").style.display = "block";
    document.getElementById("AE").classList.remove("Hidden");
    document.getElementById("OV").style.zIndex = facefull.OverlayZIndex;
    document.getElementById("AE").style.zIndex = facefull.OverlayZIndex + 1;
    document.getElementsByClassName("GlobalArea")[0].classList.add("Blur");
}

/**
 * Show custom alert.
 * @param eid
 * @constructor
 */
function AlertShowCustom(eid) {
    let e = document.getElementById(eid);
    facefull.OverlayZIndex += 5;
    document.getElementById("OV").style.display = "block";
    document.getElementById("OV").style.zIndex = facefull.OverlayZIndex;
    e.classList.remove("Hidden");
    e.style.zIndex = facefull.OverlayZIndex + 1;
    document.getElementsByClassName("GlobalArea")[0].classList.add("Blur");
}

/**
 * Hide custom alert.
 * @param eid
 * @constructor
 */
function AlertHideCustom(eid) {
    let e = document.getElementById(eid);
    facefull.OverlayZIndex -= 5;
    e.classList.add("Hidden");
    let eas = document.getElementsByClassName("Alert");
    let adflag = false;
    for (let i = 0; i < eas.length; i++) {
        if (!eas[i].classList.contains("Hidden")) {
            adflag = true;
            break;
        }
    }
    if (adflag) document.getElementById("OV").style.zIndex = facefull.OverlayZIndex;
    else {
        document.getElementById("OV").style.display = "none";
        document.getElementsByClassName("GlobalArea")[0].classList.remove("Blur");
        facefull.OverlayZIndex = 200;
    }
}

/*===================== Progressbar =====================*/

/**
 * Set position of progress bar specified by element id (pbid).
 * @param pbid
 * @param pos
 */
function setProgressbarPosition(pbid, pos) {
    if (pos > 100) pos = 100;
    else if (pos < 0) pos = 0;
    document.getElementById(pbid).children[0].style.width = pos+"%";
}

/*===================== Popup Menu =====================*/

/**
 * Popup menu UI element class. Use data-popupmenu HTML tag to set element name.
 * @param e
 * @constructor
 */
function PopupMenu(e) {
    this.epmtarget = e;
    this.epm = document.getElementById(this.epmtarget.getAttribute("data-popupmenu"));
    this.autoclose = true;
    this.onChangeState = function(state){}

    if (this.epmtarget.getAttribute("data-popupmenu-autoclose") !== undefined)
        this.autoclose = !(this.epmtarget.getAttribute("data-popupmenu-autoclose")==="0");

    this.epm.onmouseup = bind(function() {
        setTimeout(bind(function() {
            if (!this.autoclose) return;
            facefull.doCloseGlobalPopupMenu();
        }, this), 10);
    }, this);

    this.doOpenPopupMenu = function() {
        if (!this.epmtarget.classList.contains("PopupMenuTarget")) return;

        if (this.epm.classList.contains("Mobile")) {
            document.getElementById("OV").classList.add("MiddleOpacity");
            document.getElementById("OV").style.display = "block";
            document.getElementById("OV").style.zIndex = facefull.OverlayZIndex;
            this.epm.style.zIndex = facefull.OverlayZIndex + 1;
            document.getElementsByClassName("GlobalArea")[0].classList.add("Blur");
        }

        let notopenedflag = !this.isOpened();

        let did = this.epmtarget.getAttribute("data-id");
        if (did !== undefined) this.epm.setAttribute("data-id", did);

        let pos = this.epmtarget.getAttribute("data-popupmenu-pos");
        let width = parseInt(this.epmtarget.getAttribute("data-popupmenu-width"));
        if (isNaN(width)) width = 200;
        this.epm.style.width = width + "px";
        this.epm.style.display = "block";
        let rect = this.epmtarget.getBoundingClientRect();
        switch (pos) {
            default:
            case 'bottom-left':
                this.epm.style.left = rect.left + "px";
                this.epm.style.top = rect.top + this.epmtarget.offsetHeight + 10 + "px";
                break;
            case 'bottom-right':
                this.epm.style.left =  rect.left + (this.epmtarget.offsetWidth-width) + "px";
                this.epm.style.top = rect.top + this.epmtarget.offsetHeight + 10 + "px";
                break;
            case 'bottom-center':
                this.epm.style.left =  rect.left - width/2 + this.epmtarget.offsetWidth/2 + "px";
                this.epm.style.top = rect.top + this.epmtarget.offsetHeight + 10 + "px";
                break;
            case 'top-right':
                this.epm.style.left =  rect.left + (this.epmtarget.offsetWidth-width) + "px";
                this.epm.style.top = rect.top - this.epm.offsetHeight - 10 + "px";
                break;
        }

        rect = this.epm.getBoundingClientRect();
        if (rect.top+this.epm.offsetHeight > window.innerHeight) {
            this.epm.style.top = window.innerHeight - this.epm.offsetHeight - 10 + "px";
        }
        if (rect.left+this.epm.offsetWidth > window.innerWidth) {
            this.epm.style.left = window.innerWidth - this.epm.offsetWidth - 10 + "px";
        }

        facefull.LastGlobalOpenedPopupMenu = this;
        facefull.LastGlobalOpenedPopupMenuTarget = this.epmtarget;
        if (notopenedflag) this.onChangeState(true);
    }

    this.doClosePopupMenu = function() {
        if (this.epm.classList.contains("Mobile")) {
            document.getElementById("OV").classList.remove("MiddleOpacity");
            let eas = document.getElementsByClassName("Alert");
            let adflag = false;
            for (let i = 0; i < eas.length; i++) {
                if (!eas[i].classList.contains("Hidden")) {
                    adflag = true;
                    break;
                }
            }
            if (adflag) document.getElementById("OV").style.zIndex = facefull.OverlayZIndex;
            else {
                document.getElementById("OV").style.display = "none";
                document.getElementsByClassName("GlobalArea")[0].classList.remove("Blur");
                facefull.OverlayZIndex = 200;
            }
        }
        if (this.isOpened()) this.onChangeState(false);
        this.epm.style.display = "none";
    }

    /**
     * Checks if the popup menu is open.
     * @returns {boolean}
     */
    this.isOpened = function() {
        return this.epm.style.display === "block";
    }

    this.epmtarget.onclick = bind(function() {
        if (this.isOpened()) this.doClosePopupMenu();
        else this.doOpenPopupMenu();
    }, this);
}

/*===================== Tooltip =====================*/

/**
 * Tooltip UI element class.
 * @param e
 * @constructor
 */
function Tooltip(e) {
    this.etooltiptarget = e;
    this.edefaulttooltip = document.getElementById("TT");
    this.timer = null;
    this.touchtooltipshow = false;
    this.onCustomText = function(){};

    this._doTooltipInit = function() {
        let customtooltip = this.etooltiptarget.getAttribute("data-tooltip-custom-name");
        if (customtooltip !== null && customtooltip !== undefined) this.edefaulttooltip = document.getElementById(customtooltip);
        let dc = this.etooltiptarget.getAttribute("data-tooltip-text");
        let dcid = this.etooltiptarget.getAttribute("data-tooltip-textid");
        let dw = this.etooltiptarget.getAttribute("data-tooltip-width");
        this.edefaulttooltip.setAttribute("data-caption", "");
        this.edefaulttooltip.innerHTML = "";
        if (dcid && dcid !== "") this.edefaulttooltip.setAttribute("data-caption", dcid);
        if (dc && dc !== "") this.edefaulttooltip.innerHTML = dc;
        this.edefaulttooltip.style.width = dw + "px";
    }

    this.onMouseOver = function() {
        if (this.edefaulttooltip.classList.contains("Touch")) return;
        this._doTooltipInit();
        let pos = this.etooltiptarget.getAttribute("data-tooltip-pos");
        let ts = 0;

        switch (pos) {
            case 'left':
                let dw = this.etooltiptarget.getAttribute("data-tooltip-width");
                this.edefaulttooltip.style.left = this.etooltiptarget.getBoundingClientRect().left - parseInt(dw) - 30 + "px";
                ts = (this.etooltiptarget.offsetHeight-this.edefaulttooltip.offsetHeight) / 2;
                this.edefaulttooltip.style.top = this.etooltiptarget.getBoundingClientRect().top + ts + "px";
                break;
            case 'right':
                this.edefaulttooltip.style.left = this.etooltiptarget.getBoundingClientRect().left + this.etooltiptarget.offsetWidth + 10 + "px";
                ts = (this.etooltiptarget.offsetHeight - this.edefaulttooltip.offsetHeight) / 2;
                this.edefaulttooltip.style.top = this.etooltiptarget.getBoundingClientRect().top + ts + "px";
                break;
            default:
            case 'bottom':
                this.edefaulttooltip.style.top = this.etooltiptarget.getBoundingClientRect().top + this.etooltiptarget.offsetHeight + 20 + "px";
                ts = (this.etooltiptarget.offsetWidth-this.edefaulttooltip.offsetWidth) / 2;
                this.edefaulttooltip.style.left = this.etooltiptarget.getBoundingClientRect().left + ts + "px";
                break;
        }

        this.onCustomText();

        this.timer = setTimeout(bind(function() {
            this.edefaulttooltip.style.visibility = "visible";
        }, this), 800);
    };

    this.onMouseOut = function() {
        clearTimeout(this.timer);
        this.edefaulttooltip.style.visibility = "hidden";
    };

    this.onTouchStart = function() {
        this.touchtooltipshow = true;
        setTimeout(bind(function() {
            if (this.touchtooltipshow) {
                this._doTooltipInit();
                this.onCustomText();
                this.edefaulttooltip.style.top = (window.innerHeight-150) + "px";
                let ts = (window.innerWidth-this.edefaulttooltip.offsetWidth) / 2;
                this.edefaulttooltip.style.left = ts + "px";
                this.edefaulttooltip.style.visibility = "visible";
                setTimeout(bind(function() {
                    this.edefaulttooltip.style.visibility = "hidden";
                }, this), 3000);
            }
        }, this), 800);
    };

    this.onTouchEnd = function() {
        this.touchtooltipshow = false;
    };

    this.etooltiptarget.onmouseover = bind(this.onMouseOver, this);
    this.etooltiptarget.onmouseout = bind(this.onMouseOut, this);
    this.etooltiptarget.ontouchstart = bind(this.onTouchStart, this);
    this.etooltiptarget.ontouchend = bind(this.onTouchEnd, this);
}

/*===================== Pulse chart =====================*/

/**
 * Create pulse chart UI element for HTML element with specified id.
 * @param eid
 * @param values
 * @param labels
 * @param data
 */
function doCreatePulseChart(eid, values, labels, data = []) {
    this.e = document.getElementById(eid);
    this.e.innerHTML = "";
    let vmax = Math.max.apply(null, values);
    for (let i = 0; i < values.length; i++) {
        let epb = document.createElement("div");
        let epvb = document.createElement("div");
        let epv = document.createElement("div");
        let epl = document.createElement("div");
        epb.className = "PulseBlock";
        if (data.length) epb.setAttribute("data-info", data[i]);
        epvb.className = "PulseValueBox";
        epv.className = "PulseValue";
        epl.className = "PulseLabel";

        if (!vmax)
            epv.style.height = "0%";
        else
            epv.style.height = (values[i]/vmax*100)+"%";
        epl.innerHTML = labels[i];

        epvb.appendChild(epv);
        epb.appendChild(epvb);
        epb.appendChild(epl);

        this.e.appendChild(epb);
    }
}

/*===================== List =====================*/

/**
 * List UI element class. Use data-listname HTML tag to set element name.
 * @param e
 * @param mode
 * @constructor
 */
function List(e, mode = "list") {
    this.elist = e;
    this.selectable = this.elist.classList.contains("Selectable");
    this.arrowdefaultopened =  this.elist.getAttribute("data-list-defaultsubopened");
    this.sid = null;
    this.mode = mode;
    this.itemtree = [];
    this.maxlevel = 0;
    this.subitemmargin = this.elist.getAttribute("data-list-submargin");
    this.onSelect = function(id){};
    this.onCheckboxChange = function(id, state){};
    this.onOpenCloseSubItems = function(id, state){};

    this.doInit = function() {
        if (this.selectable || this.mode === "picker") {
            for (let i = 0; i < this.elist.children.length; i++) {
                this.elist.children[i].onclick = bind(function () {
                    this.doSelect(i);
                }, this);
            }
        }
    }

    /**
     * Select specified item in the list.
     * @param sid
     */
    this.doSelect = function(sid) {
        if (sid >= 0 && sid < this.elist.children.length && this.elist.children[sid].classList.contains("Disabled")) {
            this.onSelect(sid);
            return;
        }
        if (this.sid !== null && this.sid >= 0 && this.sid < this.elist.children.length) this.elist.children[this.sid].classList.remove("Selected");
        this.sid = sid;
        if (this.sid !== null && this.sid >= 0 && this.sid < this.elist.children.length) {
            this.elist.children[this.sid].classList.add("Selected");
            this.onSelect(sid);
        }
    }

    /**
     * Add new item to the list.
     * @param data
     * @param level
     * @param flags
     */
    this.doAdd = function(data = [], level = 0, flags = {checkbox: "none", action: "none"}) {
        let eli = this.mode==="picker"?document.createElement("div"):document.createElement("li");
        if (this.mode === "picker") {
            this.elist.appendChild(eli);
            return;
        }
        let lastindx = this.elist.children.length;
        let einput = null;
        if (this.maxlevel < level) this.maxlevel = level;
        for (let i in data) {
            let columndata = data[i];
            let ecolumn = "";
            if (columndata.element !== undefined) {
                eli.appendChild(columndata.element);
                ecolumn = columndata.element
            } else {
                ecolumn = document.createElement("div");
                ecolumn.innerHTML = columndata;
            }

            if (i == 0 && flags.checkbox !== undefined && flags.checkbox !== "none") {
                let name = this.elist.getAttribute("data-listname");
                let echeckbox = document.createElement("div");
                einput = document.createElement("input");
                let elabel = document.createElement("label");
                einput.type = "checkbox";
                einput.id = name+'-I'+lastindx+'-CH';
                if (flags.checkbox === "checked") einput.checked = true;
                elabel.setAttribute("for", name+'-I'+lastindx+'-CH');
                elabel.className = "Checkbox";
                ecolumn.classList.add("Checkboxed");
                elabel.appendChild(ecolumn);
                einput.onclick = bind(this.doCheckboxUpdate, this);
                echeckbox.appendChild(einput);
                echeckbox.appendChild(elabel);
                eli.appendChild(echeckbox);
            } else eli.appendChild(ecolumn);
        }

        let eaction = document.createElement("div");
        if (flags.action !== undefined && flags.action === "arrow") {
            if (this.arrowdefaultopened === undefined || this.arrowdefaultopened === null || this.arrowdefaultopened === "0") eaction.className = "Arrow";
            else eaction.className = "Arrow Opened";
            eli.addEventListener("click", bind(this.doOpenClose, this));
        } else if (flags.action !== undefined && flags.action === "popupmenu") {
            eaction.className = "Action PopupMenuTarget";
            if (flags.popupmenu_name !== undefined)
                eaction.setAttribute("data-popupmenu", flags.popupmenu_name);
            if (flags.popupmenu_pos !== undefined)
                eaction.setAttribute("data-popupmenu-pos", flags.popupmenu_pos);
            else
                eaction.setAttribute("data-popupmenu-pos", "bottom-center");
            eaction.setAttribute("data-id", lastindx);
            new PopupMenu(eaction);
        }
        eli.appendChild(eaction);

        if (this.selectable) {
            eli.addEventListener("click", bind(function() {
                this.doSelect(lastindx);
            }, this));
        }

        if (level > 0) {
            if (this.arrowdefaultopened === undefined || this.arrowdefaultopened === null || this.arrowdefaultopened === "0")
                eli.classList.add("Hidden");
            eli.classList.add("Sub");
            let margin = this.subitemmargin;
            if (margin === undefined || margin === null) margin = 30;
            eli.style.marginLeft = level*margin + "px";
            eli.setAttribute("data-list-itemlevel", level);
        } else eli.setAttribute("data-list-itemlevel", "0");

        eli.setAttribute("data-list-itemid", lastindx);
        this.elist.appendChild(eli);

        if (level > 0 && flags.checkbox !== undefined && flags.checkbox !== "none" && einput !== null) {
            this._doCheckboxRecompute(einput);
        }

        let list = this.itemtree;
        let lastlist = null;
        for (let i = 0; i < level; i++) {
            if (Array.isArray(list[list.length-1])) {
                lastlist = list;
                list = list[list.length-1];
            } else {
                list.push([]);
                lastlist = list;
                list = list[list.length-1];
                break;
            }
        }
        let current_level_index = 0;
        if (list.length-1 >= 0) {
            if (!Array.isArray(list[list.length-1]))
                current_level_index = list[list.length-1].current_level_index + 1;
            else if (list.length-2 >= 0)
                current_level_index = list[list.length-2].current_level_index + 1;
        }
        let parent_index = -1;
        if (lastlist && lastlist.length-2 >= 0) {
            parent_index = lastlist[lastlist.length-2].current_level_index;
        }
        list.push({element: eli, level: level, current_level_index: current_level_index, parent_index: parent_index});
    }

    this.doCheckboxUpdate = function(e) {
        let einput = e.target.tagName === "INPUT" ? e.target : e.target.parentElement.children[0];
        let indx = this._doCheckboxRecompute(einput);
        this.onCheckboxChange(indx, einput.checked);
    }

    this._doCheckboxRecompute = function(einput) {
        let eitem = einput.parentElement.parentElement;
        let level = parseInt(eitem.getAttribute("data-list-itemlevel"));
        let indx = parseInt(eitem.getAttribute("data-list-itemid"));
        for (let i = indx+1; i < this.elist.children.length; i++) {
            let eitemfound = this.elist.children[i];
            let levelfound = parseInt(eitemfound.getAttribute("data-list-itemlevel"));
            if (levelfound > level) {
                if (!(eitemfound.children[0].children.length > 0 && eitemfound.children[0].children[0].tagName === "INPUT")) continue;
                let einputfound = eitemfound.children[0].children[0];
                einputfound.checked = einput.checked;
            } else break;
        }

        for (let l = level; l > 0; l--) {
            for (let i = indx-1; i >= 0; i--) {
                let eitemfound = this.elist.children[i];
                let levelfound = parseInt(eitemfound.getAttribute("data-list-itemlevel"));
                if (levelfound < l) {
                    if (!(eitemfound.children[0].children.length > 0 && eitemfound.children[0].children[0].tagName === "INPUT")) continue;
                    let einputfound = eitemfound.children[0].children[0];
                    let state = 0;
                    let checkscount = 0;
                    let count = 0;
                    for (let li = i+1; li < this.elist.children.length; li++) {
                        let eitemfoundl = this.elist.children[li];
                        let levelfoundl = parseInt(eitemfoundl.getAttribute("data-list-itemlevel"));
                        if (levelfoundl === l) {
                            if (!(eitemfoundl.children[0].children.length > 0 && eitemfoundl.children[0].children[0].tagName === "INPUT")) continue;
                            let einputfoundl = eitemfoundl.children[0].children[0];
                            count++;
                            checkscount += einputfoundl.checked;
                            if (einputfoundl.indeterminate) {
                                state = 2;
                                break;
                            }
                        } else if (levelfoundl < l) break;
                    }
                    if (state !== 2) {
                        if (checkscount === count) state = 1;
                        else if (!checkscount) state = 0;
                        else state = 2;
                    }
                    if (!state) {
                        einputfound.checked = false;
                        einputfound.indeterminate = false;
                    } else if (state === 1) {
                        einputfound.checked = true;
                        einputfound.indeterminate = false;
                    } else {
                        einputfound.checked = false;
                        einputfound.indeterminate = true;
                    }
                    break;
                }
            }
        }
        return indx;
    }

    this.doOpenClose = function(e) {
        let eitem = e.target;
        if (eitem.classList.contains("Checkboxed") || eitem.tagName === "INPUT" || eitem.tagName === "LABEL") return;
        while (eitem.tagName !== "LI") eitem = eitem.parentElement;
        let eaction = eitem.children[eitem.children.length-1];
        let level = parseInt(eitem.getAttribute("data-list-itemlevel"));
        let indx = parseInt(eitem.getAttribute("data-list-itemid"));
        for (let i = indx+1; i < this.elist.children.length; i++) {
            let eitemfound = this.elist.children[i];
            let levelfound = parseInt(eitemfound.getAttribute("data-list-itemlevel"));
            if (eaction.classList.contains("Opened")) {
                if (levelfound > level) {
                    eitemfound.classList.add("Hidden");
                    if (eitemfound.children[eitemfound.children.length-1].classList.contains("Opened"))
                        eitemfound.children[eitemfound.children.length-1].classList.remove("Opened");
                }
            } else {
                if (levelfound === level+1) {
                    eitemfound.classList.remove("Hidden");
                }
            }
            if (levelfound <= level) break;
        }
        if (eaction.classList.contains("Opened")) eaction.classList.remove("Opened");
        else eaction.classList.add("Opened");
        this.onOpenCloseSubItems(indx, eaction.classList.contains("Opened"));
    }

    /**
     * Clear the list.
     */
    this.doClear = function() {
        this.elist.innerHTML = "";
        this.itemtree = [];
    }

    /**
     * Checks if list is empty.
     * @returns {boolean}
     */
    this.isEmpty = function() {
        return this.itemtree.length === 0;
    }

    /**
     * Get id of current selected item (for selectable list).
     * @returns {null|*}
     */
    this.getState = function() {
        return this.sid
    }

    /**
     * Get list item tree.
     * @returns {array|*}
     */
    this.getItemTree = function() {
        return this.itemtree;
    }

    this.getLength = function() {
        return this.elist.children.length;
    }

    /**
     * Get current selected item (for selectable list).
     * @returns {*|null}
     */
    this.getSelectedElement = function() {
        return this.sid !== null && this.sid >= 0?this.elist.children[this.sid]:null;
    }

    this.doInit();
}

/*===================== DropArea =====================*/

/**
 * DropArea UI element class. Use data-dropname HTML tag to set element name.
 * @param e
 * @constructor
 */
function DropArea(e) {
    this.eda = e;
    this.onFilesCaptured = function(filedata) {};

    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        this.eda.addEventListener(eventName, function(e) {
            e.preventDefault()
            e.stopPropagation()
        }, false);
    });

    ['dragenter', 'dragover'].forEach(eventName => {
        this.eda.addEventListener(eventName, bind(function(){
            this.eda.classList.add('Active');
        }, this), false);
    });

    ['dragleave', 'drop'].forEach(eventName => {
        this.eda.addEventListener(eventName, bind(function(){
            this.eda.classList.remove('Active');
        }, this), false);
    });

    this.doDropCapture = function(e) {
        let dt = e.dataTransfer;
        let files = dt.files;
        this.onFilesCaptured(files);
    }

    this.eda.addEventListener('drop', bind(this.doDropCapture, this), false);
}

/*===================== Tabs =====================*/

/**
 * Tabs UI element class. Use data-tabsname HTML tag to set element name.
 * @param e
 * @constructor
 */
function Tabs(e) {
    this.etabs = e;
    this.elasttab = null;
    this.lasttouchshift = 0;
    this.baseshift = 0;
    this.width = 0;
    this.selected = -1;

    this.onTabChanged = function(i){};

    this.onTouchStart = function(event) {
        let touches = event.changedTouches;
        if (touches.length >= 0) {
            this.width = 0;
            for (let i = 0; i < this.etabs.children.length; i++) {
                let style = getComputedStyle(this.etabs.children[i]);
                this.width += this.etabs.children[i].offsetWidth + parseInt(style.marginRight);
            }
            this.lasttouchshift = touches[0].pageX;
            if (!this.baseshift) {
                this.etabs.style.marginLeft = 0;
                this.baseshift = this.etabs.offsetLeft;
            }
        }
    }

    this.onTouchMove = function(event) {
        let touches = event.changedTouches;
        if (touches.length >= 0) {
            let diff = touches[0].pageX - this.lasttouchshift;
            this.lasttouchshift = touches[0].pageX;
            if (parseInt(this.etabs.style.marginLeft)+diff > 0) {
                this.etabs.style.marginLeft = 0;
                return;
            }
            let shift = -(parseInt(this.etabs.style.marginLeft)+diff);
            //console.log((this.width-shift+this.baseshift*2)-window.innerWidth)
            if (this.width-shift+this.baseshift*2 < window.innerWidth) {
                this.etabs.style.marginLeft = -(this.width-window.innerWidth+this.baseshift*2) + "px";
                return;
            }
            this.etabs.style.marginLeft = parseInt(this.etabs.style.marginLeft) + diff + "px";
        }
    }

    this.doInitTabs = function() {
        for (let i = 0; i < this.etabs.children.length; i++) {
            this.etabs.children[i].onclick = bind(function() {
                if (this.elasttab) this.elasttab.classList.remove("Selected");
                this.etabs.children[i].classList.add("Selected");
                this.elasttab = this.etabs.children[i];
                this.selected = i;
                this.onTabChanged(i);
            }, this);
        }
        this.etabs.style.marginLeft = 0;
        this.etabs.addEventListener("touchstart", bind(this.onTouchStart, this));
        this.etabs.addEventListener("touchmove", bind(this.onTouchMove, this));
    }

    /**
     * Select tab with specified id.
     * @param num
     */
    this.doSelectTab = function(num) {
        this.selected = num;
        if (num === -1) {
            if (this.elasttab) this.elasttab.classList.remove("Selected");
            this.elasttab = null;
            return;
        }
        this.etabs.children[num].onclick();
    }

    /**
     * Get id of current selected tab.
     * @returns {number|*}
     */
    this.getSelectedTab = function() {
        return this.selected;
    }

    this.doInitTabs();
}

/*===================== Circlebar =====================*/

/**
 * Circle bar UI element class. Use data-circlebarname HTML tag to set element name.
 * @param e
 * @constructor
 */
function Circlebar(e) {
    this.ecb = e;
    this.ns = "http://www.w3.org/2000/svg";
    this.ecbback = document.createElementNS(this.ns, "circle");
    this.ecbline = document.createElementNS(this.ns, "circle");
    this.elabel = document.createElement("div");

    /**
     * Set position of circle bar.
     * @param pos
     * @param label
     */
    this.setPos = function(pos, label = true) {
        if (pos > 100) pos = 100;
        else if (pos < 0) pos = 0;
        let h = this.ecb.getAttribute("data-circlebar-size");
        if (h < 25) h = 25;
        let r = (h-20)/2;
        let s = 2*Math.PI*r;
        let o = pos/100*s;
        this.ecbback.setAttributeNS(null, "r", r+"px");
        this.ecbline.setAttributeNS(null, "r", r+"px");
        this.ecbline.setAttributeNS(null, "stroke-dasharray", s);
        this.ecbline.setAttributeNS(null, "stroke-dashoffset", s-o);
        if (label) {
            this.elabel.style.display = "block";
            this.elabel.innerHTML = pos;
        } else this.elabel.style.display = "none";
    }

    this.doInit = function() {
        let ecbbody = document.createElementNS(this.ns, "svg");
        ecbbody.setAttributeNS(null, "width", "100%");
        ecbbody.setAttributeNS(null, "height", "100%");
        ecbbody.setAttributeNS(null, "class", "CircleBody");
        this.ecbback.setAttributeNS(null, "cx", "50%");
        this.ecbback.setAttributeNS(null, "cy", "50%");
        this.ecbback.setAttributeNS(null, "class", "CircleProgress CircleBack");
        this.ecbline.setAttributeNS(null, "cx", "50%");
        this.ecbline.setAttributeNS(null, "cy", "50%");
        this.ecbline.setAttributeNS(null, "class", "CircleProgress CircleLine");

        ecbbody.appendChild(this.ecbback);
        ecbbody.appendChild(this.ecbline);
        this.ecb.appendChild(ecbbody);

        this.elabel.className = "CirclebarLabel";
        this.ecb.appendChild(this.elabel);

        this.setPos(0);
    }

    this.doInit();
}

/*===================== Counter =====================*/

/**
 * Counter UI element class. Use data-countername HTML tag to set element name.
 * @param e
 * @constructor
 */
function Counter(e) {
    this.ec = e;
    this.ecback = e.children[0];
    this.ecvalue = e.children[1].children[0];
    this.ecforward = e.children[2];
    this.value = 0;
    this.backtimeout = null;
    this.backtimer = null;
    this.forwardtimeout = null;
    this.forwardtimer = null;

    this.onBeforeCount = function(direction){return true;}
    this.onAfterCount = function(direction){}

    /**
     * Decrement counter.
     */
    this.doCountBack = function() {
        if (!this.onBeforeCount(-1)) return;
        this.value--;
        this.ecvalue.value = this.value;
        this.onAfterCount(-1);
    }

    this.doStartCountBack = function() {
        if (this.backtimeout) return;
        this.backtimeout = setTimeout(bind(function () {
            if (this.backtimer) return;
            this.backtimer = setInterval(bind(function() {
                this.doCountBack();
            }, this), 100);
        }, this), 300);
    }

    this.doEndCountBack = function() {
        if (this.backtimeout && !this.backtimer) this.doCountBack();
        clearInterval(this.backtimer);
        clearTimeout(this.backtimeout);
        this.backtimer = null;
        this.backtimeout = null;
    }

    /**
     * Increment counter.
     */
    this.doCountForward = function() {
        if (!this.onBeforeCount(1)) return;
        this.value++
        this.ecvalue.value = this.value;
        this.onAfterCount(1);
    }

    this.doStartCountForward = function() {
        if (this.forwardtimeout) return;
        this.forwardtimeout = setTimeout(bind(function () {
            if (this.forwardtimer) return;
            this.forwardtimer = setInterval(bind(function() {
                this.doCountForward();
            }, this), 100);
        }, this), 300);
    }

    this.doEndCountForward = function() {
        if (this.forwardtimeout && !this.forwardtimer) this.doCountForward();
        clearInterval(this.forwardtimer);
        clearTimeout(this.forwardtimeout);
        this.forwardtimer = null;
        this.forwardtimeout = null;
    }

    this.doParseEditedValue = function() {
        this.setValue(this.ecvalue.value);
    }

    /**
     * Set counter value.
     * @param value
     * @returns {boolean}
     */
    this.setValue = function(value) {
        if (!value.toString().length) value = 0;
        if (Number.isNaN(parseInt(value))) {
            this.ecvalue.value = this.value;
            return false;
        }
        if (!this.onBeforeCount(parseInt(value)-this.value)) {
            this.ecvalue.value = this.value;
            return;
        }
        this.ecvalue.value = parseInt(value);
        this.value = parseInt(value);
        this.onAfterCount();
    }

    /**
     * Get counter value.
     * @returns {number|number|*}
     */
    this.getValue = function() {
        return this.value;
    }

    this.doInit = function() {
        if (!this.ec.classList.contains("Editable")) this.ecvalue.setAttribute("disabled", "");
        else this.ecvalue.removeAttribute("disabled");
        this.setValue(this.ecvalue.value);

        this.ecback.onmousedown = bind(this.doStartCountBack, this);
        this.ecback.onmouseup = bind(this.doEndCountBack, this);
        this.ecback.onmouseleave = bind(this.doEndCountBack, this);

        this.ecforward.onmousedown = bind(this.doStartCountForward, this);
        this.ecforward.onmouseup = bind(this.doEndCountForward, this);
        this.ecforward.onmouseleave = bind(this.doEndCountForward, this);

        this.ecvalue.oninput = bind(this.doParseEditedValue,this);
    }

    this.doInit();
}

/*===================== Counter =====================*/

/**
 * Hotkey manager UI element class. Use data-hotkeyholdername HTML tag to set element name.
 * @param e
 * @constructor
 */
function HotkeyHolder(e) {
    this.ehh = e;
    this.modkeys = {shift: false, ctrl: false, alt: false};
    this.hotkey = {mods: this.modkeys, key: ''};
    this.onHotkey = function(hotkey) {}
    this.onHotkeySet = function(hotkey) {}

    this.doInit = function() {
        this.ehh.onclick = bind(function (){
            if (this.ehh.classList.contains("Selected")) {
                this.doUnselectHolder();
            } else {
                this.doSelectHolder();
            }
        }, this);
        this.doReset();
        document.onkeydown = bind(function(event) {
            this.onHotkeyCatch(event);
        }, this);
    }

    this.doSelectHolder = function() {
        this.ehh.classList.add("Selected");
        this.doResetModKeys();
        document.onkeyup = bind(function(event) {
            this.onHotkeyUncatchMod(event);
        }, this);
    }

    this.doUnselectHolder = function() {
        this.ehh.classList.remove("Selected");
        document.onkeyup = null;
    }

    this.doResetModKeys = function() {
        this.modkeys = {shift: false, ctrl: false, alt: false};
    }

    /**
     * Reset hotkey.
     */
    this.doReset = function() {
        this.ehh.innerHTML = "<div>N/A</div>"
        this.doResetModKeys();
        this.hotkey = {mods: this.modkeys, key: ''};
        this.doUnselectHolder();
    }

    this.onHotkeyUncatchMod = function(event) {
        let key = event.keyCode;
        if (key >= 16 && key <= 18) {
            switch (key) {
                case 16:
                    this.modkeys.shift = false;
                    break;
                case 17:
                    this.modkeys.ctrl = false;
                    break;
                case 18:
                    this.modkeys.alt = false;
            }
            event.preventDefault();
        } else if (key === 46) {
            this.doReset();
            event.preventDefault();
        } else if (key === 27) {
            this.doUnselectHolder();
            event.preventDefault();
        }
    }

    this.onHotkeyCatch = function(event) {
        let key = event.keyCode;
        if (key >= 16 && key <= 18) {
            switch (key) {
                case 16:
                    this.modkeys.shift = true;
                    break;
                case 17:
                    this.modkeys.ctrl = true;
                    break;
                case 18:
                    this.modkeys.alt = true;
            }
            event.preventDefault();
        } else if (key >= 48 && key <= 90) {
            if (this.ehh.classList.contains("Selected")) {
                let modstr = '';
                if (this.modkeys.shift) modstr += '<div class="KeyMod Shift"></div><div class="Plus"></div>';
                if (this.modkeys.ctrl) modstr += '<div class="KeyMod Ctrl"></div><div class="Plus"></div>';
                if (this.modkeys.alt) modstr += '<div class="KeyMod Alt"></div><div class="Plus"></div>';
                this.ehh.innerHTML = modstr + '<div>' + String.fromCharCode(key) + '</div>';
                this.hotkey = {mods: this.modkeys, key: String.fromCharCode(key)};
                event.preventDefault();
                this.doUnselectHolder();
                this.onHotkeySet(this.hotkey);
            } else {
                if (this.hotkey.mods.shift === this.modkeys.shift
                        && this.hotkey.mods.ctrl === this.modkeys.ctrl
                        && this.hotkey.mods.alt === this.modkeys.alt
                        && this.hotkey.key === String.fromCharCode(key)) {
                    this.onHotkey(this.hotkey);
                    event.preventDefault();
                }
            }
            this.doResetModKeys();
        }
    }

    /**
     * Set hotkey.
     * @param keychar
     * @param shiftmod
     * @param ctrlmod
     * @param altmod
     */
    this.setHotkey = function(keychar, shiftmod = false, ctrlmod = false, altmod = false) {
        this.doResetModKeys();
        this.hotkey = {mods: this.modkeys, key: ''};
        let modstr = '';
        if (shiftmod) {
            this.hotkey.mods.shift = true;
            modstr += '<div class="KeyMod Shift"></div><div class="Plus"></div>';
        }
        if (ctrlmod) {
            this.hotkey.mods.ctrl = true;
            modstr += '<div class="KeyMod Ctrl"></div><div class="Plus"></div>';
        }
        if (altmod) {
            this.hotkey.mods.alt = true;
            modstr += '<div class="KeyMod Alt"></div><div class="Plus"></div>';
        }
        this.hotkey.key = keychar;
        this.ehh.innerHTML = modstr + '<div>'+keychar+'</div>';
    }

    /**
     * Get current hotkey.
     * @returns {*}
     */
    this.getHotkey = function() {
        return this.hotkey;
    }

    this.doInit();
}