/** 
 * @classdesc The main EICUI libray. Available as a global static object ui
 * @author Michael Fallise
 * @version 2.1
 * @hideconstructor
 * @category EICUI/Core 
 */
class ui {

    /** @ignore */
    static initiated = false;

    static _components = {};

    static mutations = null;

    /** 
     * Default options 
     * @property {boolean} ariaEnabled Aria support
     */
    static options = {
        ariaEnabled: true
    }
    
    /**
     *  Format helper functions
     *     @type FormatHelper 
     */
    static format = null;

    /**
     *  Growler helper 
     *     @type Growler 
     */
    static growl = null;

    /**
    *    Sets up the required layer elements used by the library
    *    @private
    *     @param options options to use when EICUI is instanciated 
    *     @param options.ariaEnabled Wheter components should automatically generate Aria attributes 
    */
    static init(options) {

        if(!ui.initiated) {

            this.layers = {};

            this.growl = new Growler(this.create('<div class="eic-growler"></div>'));
            this.layers['growler'] = this.growl.el;
            document.body.append(this.growl.el);
            
            this.layers['blocker'] = this.create('<div class="eic-global-blocker"></div>');
            document.body.appendChild(this.layers['blocker']);
            document.body.setAttribute('eicapp', '');
            
            this.mutations = new MutationObserver(ui.onDOMMutation) //no bind for static
            this.mutations.observe(document.body,  { childList: true, subtree: true })

            ui.initiated = true;
        }

        ui.format = new FormatHelper();
        //backward compatibility. please consider using ui.format instead of ui.dates
        ui.dates = ui.format
    }

    /**
    *    Locks the window prohibiting any user interaction
    */
    static lock() { this.layers['blocker'].classList.add('active'); }

    /**
    *    Unlocks the window
    */
    static unlock() { this.layers['blocker'].classList.remove('active'); }

    /**
     * Automatically scans a DOM structure and initiates known components
     * 
     * @param {Element} container a DOM node structure 
     * @returns {array<EicComponent>}
     */
    static eicfy(container) {
        let elements = [];

        let badges = container.querySelectorAll('[eicbadge]');
        for(let i = 0; i < badges.length; i++) {
            elements.push(new Badge(badges[i]));
        }

        let chips = container.querySelectorAll('[eicchip]');
        for(let i = 0; i < chips.length; i++) {
            elements.push(new Chip(chips[i]));
        }
        
        let buttons = container.querySelectorAll('[eicbutton]');
        for(let i = 0; i < buttons.length; i++) {
            elements.push(new Button(buttons[i]));
        }

        let inputs = container.querySelectorAll('[eicinput],[eiccheckbox]');
        for(let i = 0; i < inputs.length; i++) {
            let type = inputs[i].getAttribute('type');

            switch(type) {
                case 'checkbox': elements.push(new Checkbox(inputs[i])); break;
                case 'hidden': elements.push(new InputHidden(inputs[i])); break;
                case 'search': elements.push(new InputSearch(inputs[i])); break;
                case 'date': elements.push(new InputDate(inputs[i])); break;
                case 'number': elements.push(new InputNumber(inputs[i])); break;
                case 'currency': elements.push(new InputCurrency(inputs[i])); break;
                case 'toggler': elements.push(new InputToggler(inputs[i])); break;
                default: elements.push(new Input(inputs[i])); 
            }
            
        }
        
        let textareas = container.querySelectorAll('[eictextarea]');
        for(let i = 0; i < textareas.length; i++) {
            elements.push(new Textarea(textareas[i]));
        }

        let selects = container.querySelectorAll('[eicselect]');
        for(let i = 0; i < selects.length; i++) {
            elements.push(new Select(selects[i]));
        }
        
        let cards = container.querySelectorAll('article[eiccard]');
        for(let i = 0; i < cards.length; i++) {
            elements.push(new Card(cards[i]));
        }

        let dropdowns = container.querySelectorAll('[eicdropdown]');
        for(let i = 0; i < dropdowns.length; i++) {
            elements.push(new DropDown(dropdowns[i]));
        }

        let alerts = container.querySelectorAll('[eicalert][closable]');
        for(let i = 0; i < alerts.length; i++) {
            elements.push(new Alert(alerts[i]));
        }

        return elements;
    }

    /**
     * Converts a markup string into corresponding DOM structure
     * @param {string} markup 
     * @returns {DOMElement} The first root element (and its childs if any) generated by the string. 
     */
    static create(markup, options) {
        markup = markup.trim();

        let el = null;

        if(markup.indexOf('<') != 0) {
            el = document.createElement(markup);
        
            for(let prop in options) {
                switch(prop) { 
                    case 'html':
                    case 'text': 
                        el.innerHTML = options[prop];
                        break;
                    case 'class': 
                        el.className = options[prop];
                        break;
                    default: 
                        if(el.hasOwnProperty(prop)) {
                            el[prop] = options[prop];            
                        } else {
                            el.setAttribute(prop, options[prop]);
                        }
                }
            }
        } else {
            let doc  = el = document.createElement('div');
            doc.innerHTML = markup;
            el = doc.firstElementChild;
        }
        
        return el;
    }

    /**
     * 
     * @param {string} uid 
     * @returns {EicComponent|null}
     */
    static getComponent(uid) { return ui._components.hasOwnProperty(uid) ? ui._components[uid] : null; }

    /**
     * Adds a component to the registered list
     * @param {EicComponent} component 
     * @returns {string} Component's new uid 
     */
    static registerComponent(component) {
        let uid = crypto.randomUUID();
        ui._components[uid] = component;
        return uid;
    }

    /**
     * Removes a component from registered list
     * @param {string} uid 
     */
    static unregisterComponent(uid) {   
        if(ui._components.hasOwnProperty(uid))
            delete(ui._components[uid]);
    }

    /**
     * Handler for DOM MutationObserver. 
     * Scans removed nodes and unregisters embedded EICUI components
     * @param {*} mutationList 
     */
    static onDOMMutation(mutationList) {
        for(let mutation of mutationList) {
            for(let i = 0; i < mutation.removedNodes.length; i++) {
                let node = mutation.removedNodes[i]               
                // skip text nodes
                if(node.nodeName == '#text') break;
     
                if(node.hasAttribute('data-eicui-id')) 
                    ui.unregisterComponent(node.getAttribute('data-eicui-id'));
                
                let components = node.querySelectorAll('[data-eicui-id]');
                for(let component of components) 
                    ui.unregisterComponent(component.getAttribute('data-eicui-id'));
            }
        }
    }

    /**
     * Hides an Element
     * @param {Element} element 
     */
    static hide(element) { element.style.display = 'none'; }

    /**
     * Shows (unhides) an Element
     * @param {Element} element 
     * @param {boolean} [display] Forces the display property value. Default is 'block'.  
     */
    static show(element, mode) { element.style.display = mode ? mode: 'block'; }
    
    /**
     * Hides an Element in the DOM with a fade out effect.
     * @param {Element} element 
     * @param {boolean} [detach] If true, removes the element from the DOM once fade animation is completed
     */
    static fadeOut(el, detach) {
        let intId = setInterval(function () {
            if(!el.style.opacity) { el.style.opacity = 1; }
            if(el.style.opacity > 0) { 
                el.style.opacity -= 0.1; 
            } else {
                ui.hide(el);
                el.style.opacity = 1;
                clearInterval(intId);
                if(detach) { el.remove(); }
            }
        }, 20);
    }

    /**
     * Returns an element's nearest parent matching selector rule
     * @param {Element} element The source element
     * @param {string} selector The selector rule matching the targeted parent
     * @returns {Element|null} The first (bottom/up) matching parent if any
     */
    static queryParent(child, selector) {
        if (selector === undefined) {
            selector = document;
        }
        var parents = [];
        var p = child.parentNode;
        while (p !== parentSelector) {
            var o = p;
            parents.push(o);
            p = o.parentNode;
        }
        parents.push(selector);
        return parents[parents.length-1];
    }

    /**
     * Returns the actual index of an Element within its parent
     * @param {Element} element The target Element 
     * @returns {number} The element child index
     */
    static childIndex(el) {
        let childs = el.parentNode.childNodes;
        let index = 0;

        for(let i = 0; i < childs.length; i++) {
             if (childs[i] == el) return index;
             if (childs[i].nodeType == 1) index++;
        }
        return -1;
    }

    /**
     * Copies the styling properties of an Element to another
     * @param {Element} source The source Element 
     * @param {Element} target The target Element 
     */
    static copyEicuiStyling(fromEl, toEl) {
        let m
        for(let eicuiAttr of ['[danger]', '[primary]', '[secondary]', '[success]', '[warning]', '[accent]', '[info]', '[disabled]', 
                              '[xxsmall]','[xsmall]','[small]','[medium]','[large]','[xlarge]','xxl[arge','[xxxlarge]','[xxxxlarge]',
                              '.required']   ) {
            m = eicuiAttr.match(/\[(\w+)\]/)
            if(m && fromEl.hasAttribute(m[1])) toEl.setAttribute(m[1],fromEl.getAttribute(m[1]))
            m = eicuiAttr.match(/\.(\w+)/)
            if(m && fromEl.classList.contains(m[1])) toEl.classList.add(m[1])
        }
    }
}

/**
 * Special types formatting helper class
 * @hideconstructor
 * @category EICUI/Helpers
 */
class FormatHelper {
    
    defaultCurrency = '€';

    /**
     * Formats a number
     * @param {string|number} str The number to be formatted 
     * @returns {string} The formatted number
     */
    number(str) {
        str += '';
        let s = parseFloat(0 + str).toString();
        let x = s.split('.');
        let x1 = x[0];
        let x2 = (x.length > 1) ? '.' + x[1]: '';
        let r = /(\d+)(\d{3})/;
        while (r.test(x1)) {
                x1 = x1.replace(r, '$1' + ' ' + '$2');
        }
        return x1 + x2;
    }

    /**
     * Formats a currency. Uses the number() format method.
     * @param {string|number} str The amount to be formatted 
     * @param {string} [currency] The currency symbol to apply. Default is '€'.
     * @returns {string} The formatted amount
     */
    currency(str, currency) { return this.number(str) + ' ' + (currency ? currency: this.defaultCurrency) ; }

    /**
     * Formats a date string
     * @param {string} str The date string to format
     * @returns {string} The formatted date
     */
    date(str) {
        let d = new Date(str);
        return d.toLocaleDateString('en-DE', { year: 'numeric', month: 'short', day: 'numeric' });
    }

    /**
     * Formats a date and time string
     * @param {string} str The date and time string to format
     * @returns {string} The formatted date
     */
    dateTime(str) {
        let d = new Date(str);
        return d.toLocaleDateString('en-DE', { year: 'numeric', month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString('en-DE');
    }

    /**
     * Formats a time string
     * @param {string} str The time string to format
     * @returns {string} The formatted time
     */
    time(str) {
        let d = new Date(str);
        return d.toLocaleTimeString('en-DE');
    }

    url(str) {
        if(str && !/^(?:f|ht)tps?\:\/\//.test(str)) {
            str = "https://" + str;
        }
        return str;
    }

    // backward compatibility
    currency2String = this.currency
    date2String = this.date
    dateTime2String = this.dateTime
    time2String = this.time
}


/**
 * @classdesc Component base (ancestor) class
 * @category EICUI/Core
 * 
 */
class EicComponent {

    /**
     * @type {object}
     * @property {boolean} aria-enabled Aligns to global ui setting by default. see {@link ui#options}
     */
    options = {
        'aria-enabled': false
    };

    _el = null;

    /**
     * @param  {DOMElement|null} [element] a dom element attached to the component
     * @param  {Object} [options] see [options]{@link EicComponent#options}
     */
    constructor(el, options) {
        if(el && (el.getAttribute('data-eicui-id') != null) 
        && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                return(ui._components[el.getAttribute('data-eicui-id')])
        }   

        this._uid = ui.registerComponent(this);     
        this.el = el;

        this.setOptions(options || {});
        this.attributes({ 'aria-enabled': ui.options.ariaEnabled });
    } 

    get el() { return this._el; }
    set el(markup) {
        if(typeof markup == 'string') {
            const placeholder = document.createElement("div");
            placeholder.innerHTML = markup;
            const node = placeholder.firstElementChild ;
            this._el = node;
        } else {
            this._el = markup;
        }
        
        if(this._el) {
            this._el.setAttribute('data-eicui-id', this._uid);
        }
            
    }
    /**
    *    Merges passed options to component's options
    */
    setOptions(options) { this.mergeOptionsNode(this.options, options); }

    /**
    *    @ignore
    */
    mergeOptionsNode(target, source) {
        for(let option in source) {
            if(typeof source[option] === 'object' && !Array.isArray(source[option]) && (option in target) && !(target[option] instanceof(Element))) {
                this.mergeOptionsNode(target[option], source[option]);
            } else {
                target[option] = source[option];
            }    
        }
    }

    /** @type {boolean} */
    set disabled(state) { state ? this.el.setAttribute('disabled',''): this.el.removeAttribute('disabled'); }
    get disabled() { return this.el.hasAttribute('disabled'); }
    
    /**
     * Enables a component. same as obj.disabled = false
     */
    enable() { this.disabled = false; }

	/**
	 * @typedef {string} UISize
	 * Semantic UI size values. based on EUI semantic.
	 * 
	 * Available values:
	 * <pre>
	 * 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | 'xxxlarge' | 'xxxxlarge'
	 * </pre>
	 */
    /**
	 * The component size attribute. See EUI semantic size values.
	 * @type {UISize}  
	 */
    set size(value) {
        if(!this.el) return;

        let sizes = [ 'xxsmall','xsmall','small','medium','large','xlarge','xxlarge','xxxlarge','xxxxlarge' ];
        sizes.forEach(val => this.el.removeAttribute(val));

        this.el.setAttribute(value, '');
    }
    get size() { 
        if(!this.el) return('');
        for(let size of [ 'xxsmall','xsmall','small','medium','large','xlarge','xxlarge','xxxlarge','xxxxlarge' ]) {
            if(this.el.hasAttribute(size)) return(size)
        }
        return('')
    } 

	/**
	 * @typedef {string} UISeverity
	 * UI severity color code. Based EUI severity semantic. Available values:
	 * <pre>
	 * 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'accent' | 'info'
	 * </pre>
	 */
	/**
	 * The severity attribute for the component 
	 * @type {UISeverity} 
	 */
	set severity(value) {

        if(!this.el) return;

        let values = [ 'primary','secondary','success','warning','danger','accent','info' ];
        values.forEach(val => this.el.removeAttribute(val));

        this.el.setAttribute(value, '');
    }
    get severity() {
        if(!this.el) return('');
        for(let severity of [ 'primary','secondary','success','warning','danger','accent','info' ]) {
            if(this.el.hasAttribute(severity)) return(severity)
        }
        return('')
    }  

    /**
	 * Sets rounded angles for the component.  
	 * @type {boolean} */
    set rounded(value) {
        if (value) {
            this.el.setAttribute('rounded', '');
        } else {
            this.el.removeAttribute('rounded');
        }
    }

    /** @ignore */
    get position() { return this.el.getBoundingClientRect(); }

    /**
     * Pastes attributes
     * @param {*} properties 
     * @example 
     * let attr = {
     *     'data-id': 1,
     *     'data-name': 'test'
     * } 
     * obj.attributes(attr)
     * 
     */
    attributes(properties) {
        if(this.el)
            for(let property in properties) {
                this.el.setAttribute(property, properties[property]);
            }
    }

    /**
     * Hides the element
     */
    hide() {
        this._baseDisplay = this.el.style.display;
        this.el.style.display = 'none';
    }

    /**
     * Shows the element. Restores the display property value catched when using hide(), uses 'block' otherwise.
     * @todo allow forcing display value
     */
    show() { this.el.style.display = this._baseDisplay ? this._baseDisplay: 'block'; }

    /**
     * Adds an event listener
     * @param {string} type Type of event
     * @param {function} callback callback function
     */
    addEventListener(type, callback) { this.el.addEventListener(type, callback); }
}

/**
 * Alert boxes
 * @augments EicComponent
 * @category EICUI/Components
 * 
 * @todo check issue with options collision
 */
class Alert extends EicComponent {

    /** @inheritdoc */

    

    /**
     * 
     * @param {Element|null} element 
     * @param {*} options 
     */
    constructor(el, options) {

        /**
         * @type {object}
         * @property {string} [severity]
         * @property {boolean} [closable]
         * @property {string} [message]
         */
        let defaultOptions = {
            severity: '',
            closable: false,
            message: ""
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
            }   
        super(el, {...defaultOptions, ...(options || {})});

        this.setOptions(options);
        
        if(!this.el) { this.el = ui.create(`<div eicalert ${this.options.severity}>${options.message || ''}</div>`); }
        
        if(this.el.hasAttribute('closable')) this.options.closable = true;
        
        if(this.options.severity) this.severity = this.options.severity;

        if(this.options.closable) { 
            this.el.setAttribute('closable',''); 
            this.el.addEventListener('click', this.close.bind(this)); 
        }

        this.attributes({
            'aria-enabled': ui.options.ariaEnabled, 
            'role': 'status',
            'aria-label': 'Alert' 
        });
    }

    /** @ignore */
    close(event) {
        event.preventDefault();
        event.stopPropagation();

        this.el.remove();
        this.el.dispatchEvent(new Event('closed'));
    }
}

/**
 * A badge component
 * @augments EicComponent
 * @category EICUI/Components
 */
class Badge extends EicComponent {

    constructor(el, options) {
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
            }   
    
        super(el, options);

        if(!this.el) this.el = ui.create(`<span eicbadge></span>`);

        this.value = parseInt(this.el.innerHTML) || 0;
        
        if(this.options.size) this.size = this.options.size;

        this.attributes({
            'aria-enabled': ui.options.ariaEnabled, 
            'role': 'status',
            'aria-label': 'Badge' 
        });
    }

    /**
     * Value of the badge
     */
    set value(val) {
        this._value = val;
        this.el.innerHTML = this._value;
        
        if(val == 0) 
            ui.hide(this.el);
        else 
            ui.show(this.el,'inline-flex');
    }

    get value() { return this._value; }

    /**
     * Increase badge value by 1
     */
    increment() { if(this._value) this.value = this._value++; }

    /**
     * Decrease badge value by 1
     */
    decrement() { if(this._value) this.value = this._value--; }
}

/**
 * Main UI message growler. Automatically instanciated by ui and available through ui.growl
 * @see ui#growl
 * @static
 * @augments EicComponent
 * @category EICUI/Core
 * @hideconstructor
 */
class Growler extends EicComponent {

    defaultLifeTime = 6000; 

    /**
     * Growls a message
     * @param {string} message 
     * @param {string} severity 
     * @param {number} lifetime Value in ms. Default 10s.
     */
    append(message, severity, lifetime) {
        let alert = new Alert(null, { message: message, severity: severity, closable: true });
        alert.attributes({
            'aria-enabled': ui.options.ariaEnabled, 
            'role': 'status',
            'aria-label': message 
        })
        
        this.el.appendChild(alert.el);

        if(lifetime !== 0) setTimeout(this.remove.bind(this,alert), lifetime || this.defaultLifeTime);
    }

    /**
     * @ignore
     * @param {*} alert 
     * @param {*} event 
     */
    remove(alert, event) { ui.fadeOut(alert.el, true); }
}

/**
 * A card container. Cards contain a title header, a content section and optionally a footer. 
 * @augments EicComponent
 * @category EICUI/Components
 * 
 */
class Card extends EicComponent {
    /**
     * 
     * @param {Element|null} element 
     * @param {object} options see [options]{@link Card#options}
     */
    constructor(el, options) {
        /**
         * 
         * @type {object}  
         * @property {boolean} options.collapsable 
         * @property {boolean} options.collapsed 
         * @property {boolean} options.busy 
         */
        let defaultOptions = {
            collapsable: false,
            collapsed: false,
            busy: false
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
            }   
    
        super(el, {...defaultOptions, ...(options || {})});

        this.setOptions(options);
        this.header = this.el.querySelector('header');
        this.content = this.el.querySelector('section');
        this.footer = this.el.querySelector('footer');

        if(this.el.hasAttribute('collapsable')) this.options.collapsable = true;
        if(this.el.hasAttribute('collapsed')) this.options.collapsed = true;

        if(this.options.collapsable) {
            this.expander = ui.create(`<button eicbutton rounded class="collapser icon-angle-up"></button>`);
            this.expander.addEventListener('click', this.toggle.bind(this));
            this.header.appendChild(this.expander);

            if(this.options.collapsed)
                this.collapse();
            else 
                this.expand();
        }
    }
    
    /** @type {string} */
    set title(str) { if(this.header) this.header.querySelector('h1').innerHTML = str; }
    
    /** @type {string} */
    set subtitle(str) { if(this.header) this.header.querySelector('h2').innerHTML = str; }
    
    /** @type {boolean} */
    get collapsed() { return this.options.collapsed; }

    /** @type {boolean} */
    set loading(value) {
        
        if(value) {
            this.el.classList.add('loading')
        } else {
            this.el.classList.remove('loading')
        }
    }
    
    /**
     * 
     */
    clear() {}

    /**
     * 
     * @param {*} event 
     */
    toggle(event) {
        event.stopPropagation();
        event.preventDefault();
    
        if(this.options.collapsed){
            this.expand();
        } else {   
            this.collapse();
        }
    }

    /**
     * 
     */
    collapse() {
        this.expander.classList.remove('arrow-expand', 'arrow-collapse');
        void this.expander.offsetWidth;
        this.expander.classList.add('arrow-collapse');
        this.el.setAttribute('collapsed','');
        this.content.classList.add('collapsed');
        this.options.collapsed = true;
    }

    /**
     * 
     */
    expand() {
        this.expander.classList.remove('arrow-expand', 'arrow-collapse');
        void this.expander.offsetWidth;
        this.expander.classList.add('arrow-expand');
        this.el.removeAttribute('collapsed');
        this.content.classList.remove('collapsed');
        this.options.collapsed = false;
    }
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
 class Chip extends EicComponent {

    constructor(el, options) {
        let defaultOptions = {
            label: null,
            severity: '',
            size: '',
            icon: null,
            badge: null,
            destroyable: false,
            data: {}
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }   
        super(el, {...defaultOptions, ...(options || {})});

        if(!this.el) 
            this.el = ui.create(`<span eicchip><label>${this.options.label}<label></span>`);

        for(let parm in this.options.data) {
            this.el.setAttribute('data-' + parm, this.options.data[parm]);
        }

        if(this.el.hasAttribute('destroyable')) this.options.destroyable = true;
        if(this.el.hasAttribute('severity')) this.options.severity = this.el.getAttribute('severity');
        if(this.el.hasAttribute('size')) this.options.severity = this.el.getAttribute('size');
        if(this.el.hasAttribute('destroyable')) this.options.destroyable = true;
        if(this.el.hasAttribute('icon')) this.options.icon = this.el.getAttribute('icon');
        if(this.el.hasAttribute('badge')) this.options.badge = this.el.getAttribute('badge');

        if(this.options.destroyable) {
            let button = ui.create(`<button class="close"><i class="icon-close"></i></button>`);
            button.addEventListener('click', this.onDestroy.bind(this));
            this.el.appendChild(button);
        }

        if(this.options.icon) { this.el.appendChild(ui.create(`<i class="${this.options.icon}"></i>`));    }
        if(this.options.badge) { 
            this.badge = new Badge(null, {size: 'xxsmall'});
            this.el.appendChild(this.badge.el);    
            this.badge.value = this.options.badge;
        }
        if(this.options.severity) { this.severity = this.options.severity;    }
        if(this.options.size) { this.size = this.options.size;    }
        if(this.options.icon) { this.el.appendChild(ui.create(`<i class="${this.options.icon}"></i>`));    }
        
        this.attributes({
            'aria-enabled': ui.options.ariaEnabled, 
            'role': 'status',
            'aria-label': 'Chip' 
        });
    }

    onDestroy(event) {    
        event.stopPropagation();
        event.preventDefault();

        this.el.parentElement.removeChild(this.el);
        this.el.dispatchEvent(new CustomEvent('destroy', {detail: this.options.data}));
        this.onDestroyed(this)
    }

    onDestroyed(chip) { }

}


/**
 *    Tab Controlled Content component
 * @category EICUI/Components
 * @author Michael Fallise
 * @version 1.0
 */
class Tab { 
    
    /**
    *    @constructor
    */
    constructor() {

        this.triggers = [];
        this.contents = [];
    }

    /**
    * Links a new set of tab control and content to the collection
    * @param {Object} trigger a DOM element used to requet the content 
    * @param {Object} content a DOM element to be displayed by the trigger 
    */
    addTab(trigger, content) {
        this.triggers.push(trigger);
        this.contents.push(content);
        ui.hide(content);
        trigger.setAttribute('tab-id', this.triggers.length);
        trigger.addEventListener('click',this.onTriggerClick.bind(this));
    }

    /**
    * Links an array of tab controls to an array of contents to the collection
    * @param {Array} trigger a DOM element used to requet the content 
    * @param {Array} content a DOM element to be displayed by the trigger
    */
    addTabs(triggers, contents) {
        if(triggers.length > 0) {
            for(let i = 0; i < triggers.length; i++) {
                this.addTab(triggers[i], contents[i])
            }
            this.selectByIndex(0);
        }
    }

    /**
    * click event handler
    * @param {Object} event a mouse event object fired by the trigger element
    */
    onTriggerClick(event) {
        
        var button = event.currentTarget;

        for(var i = 0; i < this.triggers.length; i++) { 
            if(this.triggers[i].getAttribute('tab-id') == button.getAttribute('tab-id')) {
                this.selectByIndex(i);
                break;
            }
        }
    }

    /**
    * Activates toggler corresponding to the provided index
    * @param {number} index the index of the trigger to activate
    */
    selectByIndex(index) {
        for(var i = 0; i < this.triggers.length; i++) { 
            this.triggers[i].classList.remove('tab-selected');
            ui.hide(this.contents[i]);
        }
        this.triggers[index].classList.add('tab-selected');
        ui.show(this.contents[index]);
        this.triggers[index].dispatchEvent(new Event('selected'));
    }

    /**
    * Activates toggler containing provided class
    * @param {string} classname the css class name of the trigger to activate
    */
    selectByClass(classname) {
        for(var i = 0; i < this.triggers.length; i++) { 
            if(this.triggers[i].classList.has(classname)) {
                this.triggers[i].classList.add('tab-selected');
                ui.show(this.contents[i]);
            } else {
                this.triggers[i].classList.remove('tab-selected');
                ui.hide(this.contents[i]);
            }
        }
    }

    /**
    * Retrieves the index of the active trigger
    * @return {number} the current index
    */
    getSelectedIndex() {
        for(var i = 0; i < this.triggers.length; i++) { 
            if(this.triggers[i].classList.contains('tab-selected')) return i;
        }

        return -1;
    }

    /**
    * Retrieves the active trigger
    * @return {Element} the current trigger DOM Element
    */
    getSelected() { return this.triggers.find(el => el.classList.contains('tab-selected')); }

    /*
    * Disables the toggler corresponding to the provided index
    * @param {integer} index the toggler index to activate
    * 
    */
    disableByIndex(index) {

        let trigger = this.triggers[index];
        trigger.setAttribute('disabled', '');
    }
}

/**
 * Button component
 * @augments EicComponent
 * @category EICUI/Components
 */
class Button extends EicComponent {

    

    constructor(el, options) {
        let defaultOptions = {
            icon: null,
            severity: '',
            disabled: false,
            badge: false,
            rounded: false,
            size: null,
            hint: null,
            label: '',
            onclick: null
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }      
        super(el, {...defaultOptions, ...(options || {})});

        if(!this.el) { this.el = ui.create('<button eicbutton></button>'); }
        if(this.el.hasAttribute('icon')) this.options.icon = this.el.getAttribute('icon');
        if(this.el.hasAttribute('disabled')) this.options.disabled = true;
        if(this.el.hasAttribute('badge')) this.options.badge = this.el.getAttribute('badge') || true;

        if(options && options.label) this.label = options.label;
        if(options && options.severity) this.severity = options.severity;

        if(this.options.icon) { 
            let icon = this.el.querySelector('i');
            if(icon) icon.remove();
            this.el.appendChild(ui.create(`<i class="${this.options.icon}"></i>`));    
        }

        if(this.options.rounded) { this.el.setAttribute('rounded', '');    }
        if(this.options.size) { this.el.setAttribute(this.options.size, '');    }
        if(this.options.hint) { this.el.setAttribute('title', this.options.hint);    }

        if(this.options.badge) { 
            this.badge = this.el.querySelector('[eicbage]');
            if(this.badge) this.badge.remove();
            this.badge = new Badge(ui.create(`<span eicbadge danger>${this.options.badge}</i>`));
            this.el.appendChild(this.badge.el);    
        }

        if(options && options.onclick) this.click = options.onclick;    
            
        this.disabled = this.options.disabled;

        this.attributes({
            'aria-enabled': ui.options.ariaEnabled, 
            'role': 'button',
            'aria-label': this.el.getAttribute('title') || 'Button' 
        });

        if(this.el.hasAttribute('loading')) this.loading = this.el.getAttribute('loading');

        this.el.addEventListener('click', this.onClick.bind(this));
    }

    set label(label) {
        this.options.label = label
        this.el.innerHTML = `<span>${label}</span>`;
    }

    set hint(hint) {
        this.options.hint = hint
        this.el.setAttribute('title', hint)
    }

    set loading(value) {
        let icon = this.el.querySelector('i');

        if(icon) {
            icon.className = value ? 'icon-spinner spin': this.options.icon;
        } else {
            this.el.setAttribute('loading', value);
        }

        this.disabled = value;
    }

    show() {
        this.el.style.display = 'inline-flex';
    }

    onClick(event) {
        event.stopPropagation();
        event.preventDefault();

        this.click(event);
    }

    // By Nike: be silent by default in places with old-style addEventListener shuts-up :-)  
    // To trace those places : change it to { console.log(event); console.trace() }
    click(event) { } 
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
class Select extends EicComponent {
    
    /**
    * @param {element|string} el A SELECT element or the selector string for the SELECT which will be binded to the component. 
    *      This passed SELECT element will be hidden (display: none) by the wrapper but will still accessible by the DOM. 
    *      The component will use the OPTION and OPTGROUP nodes contained in the parent SELECT to feed its list. 
    * @param {object} [options] Component options:
    * @param {boolean} [options.multi] Set to true to enable mutiple value selection. Can also be enabled by adding a "mutiple" class to the original SELECT element
    * @param {boolean} [options.lookup] Set to true to enable value lookup (search engine). Can also be enabled by adding a "lookup" class to the original SELECT element
    * 
    */

    listenVanilla = true;
    activeEditor = SelectEditor;

    constructor(el, options) {
        let defaultOptions = { 
            multi: false,
            editable: false, 
            lookup: false, 
            maxItems: 0,
            placeholder: '',
            hint: '',
            editor: null
        }

        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }      
        super(el, {...defaultOptions, ...(options || {})});
        let self = this;

        if(el.hasAttribute('placeholder')) this.options.placeholder = el.getAttribute('placeholder');
        if(el.hasAttribute('hint')) this.options.hint = el.getAttribute('hint');
        if(el.hasAttribute('multiple')) { this.options.multi = true; }
        if(el.hasAttribute('lookup')) { this.options.lookup = true; }
        if(el.hasAttribute('editable')) { 
            this.options.editable = true; 
            this.options.multi = true;
            el.setAttribute('multiple', '')
        }

        this._value = [];
        this.selectedItems = [];

        this.coat(el);
        this.disabled = el.hasAttribute('disabled');
        
        // sync selected options
        el.querySelectorAll(':checked').forEach( (el) => el.value ? this.selectedItems.push({value: el.value || el.text, label: el.text}): el=el );
        
        this.catalog = !this.options.editable ? this.getCatalog(): null;

        //ui.hide(this.catalog);
        
        // tracks changes if vanilla select is modified 
        el.addEventListener('change', this.onVanillaChange.bind(this))
        //el.addEventListener('DOMNodeInserted', this.onVanillaChange.bind(this))
        
        this.mutationList = { attributes: true, childList: true, subtree: false };
        this.observer = new MutationObserver(this.onVanillaChange.bind(this));
        this.observer.observe(el, this.mutationList);

        this.selection.addEventListener('remove', function() { ui.hide(self.catalog); });

        this.update();

        if(this.options.editable) {
            this.selection.addEventListener('click', this.onOpenEditor.bind(this));
        }
        this.visibilityObserver = new IntersectionObserver( this.onVisibilityChanged.bind(this), {root: null, rootMargin: "0px", threshold:0})
    }

    coat(el) {

        if(this.el.classList.contains('required')) {
            this.options.hint = 'This field is required. ' + this.options.hint; 
        }

        this.container = ui.create(`<div class="eicui-input-container"></div>`)
        if(el.parentNode) el.parentNode.insertBefore(this.container, el);

        this.container.append(this.el);

        this.selection = ui.create(`<div class="eicui-select-selection ${this.el.getAttribute('class')}"></div>`);
        //this.selection.classList.remove('eicui-select');
        this.container.append(this.selection);

        if(this.options.hint != '') {
            this.hint = ui.create(`<div class="eicui-input-hint">${this.options.hint}</div>`);
            this.container.append(this.hint);
        }

        this.selection.setAttribute('placeholder', this.options.placeholder)

        ui.hide(el);
    }

    /**
     * Vanilla Select value change handler (value assignment goes upstream)
     * @param {*} event 
     */
    onVanillaChange(event) { 

        if(this.listenVanilla) {

            let options = this.el.querySelectorAll('option[selected="selected"], option[selected]')
            //if(options.length > 0) this.value = Array.prototype.slice.call(options).map(el => el.value);
        }
    }

    getCatalog() {
        let catalog = document.querySelector('#eicui-select-catalog');

        // Build catalog window if it has not been set up already (catalog window is shared between all select components)
        if(!catalog) {
            catalog = ui.create(`<div id="eicui-select-catalog" class="eicui-select-catalog"></div>`);
            document.body.appendChild(catalog);

            catalog.appendChild(ui.create(`<div class="content"></div>`));
            
            // close catalog when loosing focus
            window.addEventListener('click', this.closeCatalog.bind(this));
            catalog.addEventListener('click', function(event) { event.stopPropagation(); });
        }

        this.selection.addEventListener('click', this.onOpenCatalog.bind(this));

        return catalog
    }

    onOpenCatalog(event) {
        event.stopPropagation();
        this.openCatalog(this.options.lookup);
    }

    /**
    *    Opens the items selection dialog 
    */
    openCatalog(withLookup) {
        // resetting lookup input
        let search = this.catalog.querySelector('.search');
        if(search) {
            search.remove();
        }

        this.searchInput = ui.create(`<input type="text" class="search" />`); 
        this.searchInput.addEventListener('keyup',this.lookup.bind(this));
        this.catalog.prepend( this.searchInput);

        if(withLookup) {
            this.catalog.classList.add('lookup');
        }
        else {
            this.catalog.classList.remove('lookup');
        }

        search = this.catalog.querySelector('.search');
        search.value = '';
        
        let content = this.catalog.querySelector('.content');
        content.innerHTML = '';
        
        this.loadItems(this.el, content);
        this.catalog.style.left = this.offset.left + 'px';
        this.catalog.style.top = this.offset.top + 'px';
        this.catalog.width = this.selection.getBoundingClientRect().width + 'px';
        ui.show(this.catalog);
        this.catalog.setAttribute('opened', '');
        this.visibilityObserver.observe(this.container)
    }

    
    /**
    *    Event handler for selection list closing. Can be used as a standard method when no event is provided.
    * @private
    */
    closeCatalog(event) {
        if(this.catalog.hasAttribute('opened')) {
            if(event) {
                event.stopPropagation();
                event.preventDefault();
            }
            ui.fadeOut(this.catalog);
            this.catalog.removeAttribute('opened');
        }
        if(this._keyPressed) this.catalog.removeEventListener('keypressed', this._keyPressed)
        this.visibilityObserver.disconnect()
    }

    /**
    *    Event handler called when the (coated) selector visibility changes. 
        *   Used to make sure to close the catalog when the selector dissapears for any reason.
    * @private
    */
    onVisibilityChanged(intersections, obs){
        if(intersections[0].isIntersecting) return
        // This closes the catalog also when scrolling-out the selector. (not bad as catalog is floating)
        // if you still want to avoid this, add some condition on the computed visibility of this.container 
        if(this.catalog.hasAttribute('opened')) this.closeCatalog()
    }

    /**
     * 
     * @returns {Object}
     */
    getEditor() {
        
        if(!this.editor) {
            this.editor = new this.activeEditor();
            this.editor.initUI();
            this.editor.initEvents();
            this.selection.after(this.editor.el);
            
            window.addEventListener('click', this.editor.onClose.bind(this.editor));
            this.editor.addEventListener('click', function(event) { event.stopPropagation(); });
        }
        return this.editor;
    }

    /**
     * 
     * @param {*} event 
     */
    async onOpenEditor(event) {
        event.stopPropagation();
        
        let response = await this.openEditor();

        if(response) { this.addItem(response); }
    }

    onEditorOpened() {}

    /**
     * 
     */
    openEditor() {
        
        let self = this;
    
        let promise = new Promise(function(resolve, reject) {
            self.listenVanilla = false;
            self.catalog = self.getEditor();
            self.catalog.open();
            self.onEditorOpened(self.catalog);
            self.catalog.el.querySelector('input').value = "";
            self.catalog.el.querySelector('input').focus();

            function commit(result) {
                self.catalog.close();
                resolve(result);
            }

            function abort(result) {
                self.catalog.close();
                reject(result);
            }

            self.catalog.commit = commit;
            self.catalog.abort = abort;
        });

        return promise;
    }

    /**
     * 
     */
    closeEditor() {
        ui.hide(this.catalog.el);
        this.catalog.el.removeAttribute('opened', '');
        //this.listenVanilla = true;
    }

    /**
     * 
     * @param {*} event 
     */
    onKeyPressed(event) {
        event.stopPropagation();
        if(event.keyCode == 13) {
            this.addItem(this.catalog.el.querySelector('input').value); 
            this.closeEditor();
        }
    }
    
    /**
     * 
     */
    set disabled(value) {
        if(value)
            this.selection.setAttribute('disabled','');
        else
            this.selection.removeAttribute('disabled');
    }

    /**
     * 
     * @returns NodeList
     */
    get items() { return this.el.querySelectorAll('options'); }

    /**
     * Finds Y axis position of element in the page
     * @returns number
     */
    get offset() {
        if (!this.selection.getClientRects().length) {
            return { top: 0, left: 0 };
        }

        let rect = this.selection.getBoundingClientRect();
        let win = this.selection.ownerDocument.defaultView;
        return ( { top: rect.top + win.scrollY, left: rect.left + win.scrollX });   
    }

    /**
     * Returns selected value(s) 
     * @returns {string|Array}
     */
    get value() { 
        return this.options.multi ? this._value: this._value.length > 0 ? this._value[0]: null ; 
    }

    set value(val) {     
        if(val === this._value) return;

        if(typeof val == "boolean") {
            val = val.toString();
        }

        if(val === '' || val === null) {
            this.clear();
            return;    
        }
            
        if(this.options.multi) {
            if(!Array.isArray(val))    {
                val = val.split(',');
            }
        } else {
            val = val != '' ? [ val ]: [];
        }

        this.selectedItems = [];

        for(let i = 0; i < val.length; i++) {
            let option
            if(this.options.editable) {
                option = ui.create('<option value="' + val[i] + '" selected="selected">"' + val[i] + '"</option>');
                this.listenVanilla = false;
                this.el.append(option);
                //this.listenVanilla = true;
            } else {
                option = this.el.querySelector('option[value="' + val[i] + '"]');
            }
            if(val[i] != '' && option)
                this.selectedItems.push({value: val[i], label: option.label});
        }
        
        this.update();    

        return;
    }

    /**
    * Wrapper for jQuery element.change() event handler method
    * Triggered when the selection is changed (through a user interaction).
    * @param {function} handler The call back function called when event is triggered
    */
    change(handler) { this.el.addEventListener('change', handler); }

    /**
    *    Selects an item in the collection
    * @param {item} The selected LI element 
    * @param {boolean|null} forcedValue if set, selects (with true) or unselects (with false) the item. If null, toggles the selection state.
    */
    select(item, forcedValue) {
        
        this.listenVanilla = false;
        // check if max selection reached
        if(this.options.multi && this.options.maxItems > 0 && !item.classList.contains('selected')) {
            if(this.selectedItems.length == this.options.maxItems) return;
        }
        
        if(forcedValue == null) {
            item.classList.toggle('selected');
        } else {
            if(forcedValue) {
                item.classList.add('selected');
            } else {
                item.classList.remove('selected');
            }
        }

        forcedValue = item.classList.contains('selected');

        let value = item.getAttribute('data-value');
        let label = item.querySelector('span');
        let childs = item.querySelectorAll('ul > li');

        if(item.classList.contains('selected')) {

            if(!this.options.multi) { 
                this.selectedItems = [];
                this.catalog.querySelector('.selected').classList.remove('selected');
                item.classList.add('selected');
                this.closeCatalog();
            }

            if(this.selectedItems.findIndex(x => x.value == value) == -1) {
                this.selectedItems.push({value: value, label: label.innerHTML});
            }
        } else { 
            var index = this.selectedItems.findIndex(x => x.value == value);

            this.selectedItems.splice(index, 1);
        }

        for(var i = 0; i < childs.length ; i++) { 
            this.select(childs[i], forcedValue);
        }

        this.update();

        this.listenVanilla = true;
    }

    /**
    *    Event handler for item removal from selection
    */
    remove(event) {
        event.stopPropagation();
        event.preventDefault();
        
        this.listenVanilla = false;

        if(this.options.multi) {
            let id = event.detail.id;
            this.selectedItems.splice(this.selectedItems.findIndex(x => x.value == id), 1);
        } else {
            this.clear();
        }

        this.update();

        this.listenVanilla = true;
    }

    /**
    *    Remove all options
    */
    empty() {
        this.el.innerHTML = '';
        this.clear();
    }

    /**
    *    Clears the selection
    */
    clear(noevent) {
        this._value = '';
        this.selectedItems = [];
        this.update(noevent);
    }

    addItem(newItem) {
        this.listenVanilla = false;
        if(this.options.maxItems > 0 && this.selectedItems.length == this.options.maxItems) return;
        if(!this.selectedItems.find(item => item.value == newItem.value)) {
            this.selectedItems.push(newItem);
            let option = this.el.querySelector(`option[value="${newItem.value}"]`)
            if(!option) {
                option = ui.create(`<option value="${newItem.value}">${newItem.label}</option>`)
                this.el.append(option);
            }
            option.setAttribute('selected','selected')
            this.update();
        }

        //this.catalog.el.querySelector('input').value = "";
    }

    /*
    *    Fills catalog window with the related SELECT.OPTION fields content (recurses on optgroup subsets)
    * @private
    */
    loadItems(el, container, filter) {
        let self = this;
        let tags = el.children;
        let list = [];
        let ul = ui.create('<ul/>');
        let latest = null;

        this.listenVanilla = false;

        container.appendChild(ul);
        
        for(var i = 0; i < tags.length; i++) { 
            let tag = tags[i];

            if(!filter || filter == '') {
                if(tag.nodeName == 'OPTION' && tag.getAttribute('value')) {
                    let li = ui.create(`<li data-value="${tag.getAttribute('value') || tag.innerText}" ${tag.getAttribute('data-ref') ? `data-ref="${tag.getAttribute('data-ref')}"`: ''}><span>${tag.innerText}</span></li>`);
                    li.addEventListener('click',function(event) {
                        event.stopPropagation();
                        event.preventDefault();
                        self.select(this);
                    });

                    ul.appendChild(li);
                    
                    latest = li;
                    if(this.selectedItems.findIndex(x => x.value == tag.getAttribute('value')) != -1) li.classList.add('selected');
                }

                if(tag.nodeName == 'OPTGROUP') {
                    let lane = latest
                    if(tag.getAttribute('data-parent') != '') {
                        lane = this.catalog.querySelector('.content').querySelector('li[data-ref="' + tag.getAttribute('data-parent') + '"]')
                        ul.appendChild(lane);
                    }
                    
                    this.loadItems(tag, lane, filter);
                }

            } else {

                if(tag.nodeName == 'OPTION' && tag.getAttribute('value')) {

                    if(tag.innerText.toLowerCase().indexOf(filter.toLowerCase()) != -1) {
                        let li = ui.create(`<li data-value="${tag.getAttribute('value') || tag.innerText}" ${tag.getAttribute('data-ref') ? `data-ref="${tag.getAttribute('data-ref')}"`: ''}><span>${tag.innerText}</span></li>`);
                        li.addEventListener('click',function(event) {
                            event.stopPropagation();
                            event.preventDefault();
                            self.select(this);
                        });
    
                        ul.appendChild(li);
                        latest = li;
                        if(this.selectedItems.findIndex(x => x.value == tag.getAttribute('value')) != -1) li.classList.add('selected');
                    }
                }

                if(tag.nodeName == 'OPTGROUP') { this.loadItems(tag, ul, filter); }
            }
        }

        this.listenVanilla = true;
        
        return list;
    }

    /*
    *    Filters to catalog window values
    * @private
    */
    lookup(event) {
        let content = this.catalog.querySelector('.content');
        content.innerHTML = '';
        this.loadItems(this.el, content, event.target.value);
    }

    /**
    *    Refreshes displayed selection with stored values
    * @private
    */
    update(silent) {
        this.selection.innerHTML = this.selectedItems.length == 0 ? this.options.placeholder:'';
        this._value =  [];

        this.listenVanilla = false;
        // resetting all select options
        this.el.querySelectorAll('option[selected="selected"], option[selected]').forEach(el => el.removeAttribute('selected'));
        for(let i = 0; i < this.selectedItems.length; i++) { 
            if(this.selectedItems[i].value != "") {
                this.el.querySelector('option[value="' + this.selectedItems[i].value + '"]').setAttribute('selected', 'selected');
                if(this.options.multi || this.options.editable ) {
                    let item = new Chip(null,{label: this.selectedItems[i].label, destroyable:true, size: 'small', data: {id: this.selectedItems[i].value}})
                    item.addEventListener('destroy',this.remove.bind(this));
                    this.selection.appendChild(item.el);
                } else {
                    let item = ui.create(`<span class="item"><span>${this.selectedItems[i].label}</span><button class="clear"><i class="icon-close"></i></button></span>`)
                    let button = item.querySelector('button');
                    button.addEventListener('click',this.remove.bind(this));
                    this.selection.appendChild(item);
                }
                
                this._value.push(this.selectedItems[i].value);
            }
        }
        
        if(!silent) this.el.dispatchEvent(new CustomEvent('change'));

        this.listenVanilla = true;
    }
}

class Select2 extends EicComponent {

    

    selectedItems = []

    constructor(el, options) {
        let defaultOptions = { 
            multi: false,
            editable: false, 
            lookup: false, 
            maxItems: 0,
            placeholder: '',
            hint: '',
            editor: null
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});

        if(el.hasAttribute('placeholder')) this.options.placeholder = el.getAttribute('placeholder');
        if(el.hasAttribute('hint')) this.options.hint = el.getAttribute('hint');
        if(el.hasAttribute('multiple')) { this.options.multi = true; }
        if(el.hasAttribute('lookup')) { this.options.lookup = true; }
        if(el.hasAttribute('editable')) { 
            this.options.editable = true; 
            this.options.multi = true;
            el.setAttribute('multiple', '')
        }

        this.selectedItems = [];

        this.coat(el);

        this.disabled = el.hasAttribute('disabled');
        
        // sync selected options
        //el.querySelectorAll(':checked').forEach( (el) => el.value ? this.selectedItems.push({value: el.value || el.text, label: el.text}): el=el );
        // tracks changes if vanilla select is modified 
        //el.addEventListener('change', this.onVanillaChange.bind(this))
        //el.addEventListener('DOMNodeInserted', this.onVanillaChange.bind(this))
        //this.mutationList = { attributes: true, childList: true, subtree: false };
        //this.observer = new MutationObserver(this.onVanillaChange.bind(this));
        //this.observer.observe(el, this.mutationList);
        //this.selection.addEventListener('remove', function() { ui.hide(self.catalog); });

        this.update();

        this.selection.addEventListener('click', this.onMenuOpen.bind(this));
    }

    coat(el) {

        if(this.el.classList.contains('required')) {
            this.options.hint = 'This field is required. ' + this.options.hint; 
        }

        this.container = ui.create(`<div class="eicui-input-container"></div>`)
        if(el.parentNode) el.parentNode.insertBefore(this.container, el);

        this.container.append(this.el);

        this.selection = ui.create(`<div class="eicui-select-selection ${this.el.getAttribute('class') || ''}"></div>`);
        
        this.container.append(this.selection);

        if(this.options.hint != '') {
            this.hint = ui.create(`<div class="eicui-input-hint">${this.options.hint}</div>`);
            this.container.append(this.hint);
        }

        this.selection.setAttribute('placeholder', this.options.placeholder)

        ui.hide(el);
    }

    onMenuOpen(event) {
        event.stopPropagation();
        event.preventDefault();
    }

    onMenuClose(event) {
        event.stopPropagation();
        event.preventDefault();
    }

    addOption(option, selected) {
        let tag = ui.create(`<option value="${option.value}" ${selected ? 'selected=selected': ''}>${option.label}</option>`)
        this.el.append(tag);

        if(selected) this.el.dispatchEvent(new CustomEvent('change'))
    }

    loadOptions() {    this.selectedItems = []; }

    saveOptions() {    }

    get value() {
        return (
            this.options.multiple ? 
                this.selectItems.map(item => item.value)  : 
                this.selectItems.length != 0 ? 
                    this.selectItems[0].value: 
                    null
        )
    }

    set value(val) {}

    clear() {
        this.selectedItems = [];
        this.saveOptions();
        this.refresh();
    }

    refresh() {    }

}

/**
 * @augments EicComponent
 * @category EICUI/Plugins/Select
 */
class SelectEditor extends EicComponent {
    
    _template = `<div class="eicui-select-editor"><input eicinput data-type="ignore" /></div>`;
    _value = '';

    constructor(el, options) {
        super();

        if(el) this._template = el;

        this.setOptions(options);
        
    }

    initUI() {
        if(!this.el) {
            this.el = ui.create(this._template);
            this.components = ui.eicfy(this.el);
        }
    }

    initEvents() { this.el.querySelector('input').addEventListener('keypress', this.onKeyPressed.bind(this)); }

    get valid() { return this.el.querySelector('input').value.trim != ''; }

    get value() { return { value: this.el.querySelector('input').value, label: this.el.querySelector('input').value } }

    /**
     * 
     * @param {*} event 
     */
    onKeyPressed(event) {
        event.stopPropagation();
        if(event.keyCode == 13 && this.valid) this.commit(this.value); 
    }

    open() {
        

        ui.show(this.el, 'grid');

        
    }

    onClose(event) {
        event.stopPropagation();
        this.commit();
    }

    close() { ui.hide(this.el); }

    
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
class Menu extends EicComponent {
    constructor(el, options) {
        let defaultOptions = {
            collapsable: true,
            lookup: false,
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});

        if(!el) this.el = ui.create(`<menu eicmenu></menu>`);

        if(el.hasAttribute('lookup')) this.options.lookup = true;

        if(this.options.lookup) {
            this.search = ui.create(`<li class="menu-lookup"><input eicinput type="search" placeholder="Search filter" /></li>`);
            this.search.querySelector('input').addEventListener('keyup', this.filter.bind(this))
            this.el.append(this.search);
        }
    }

    get collapsed() { return this.el.hasAttribute('collapsed'); }
    set collapsed(value) { 
        if(value) 
            this.el.setAttribute('collapsed', '');
        else 
            this.el.removeAttribute('collapsed');
    }

    parse(data, node) {
        
        node = node || this.el;
        for(let i = 0; i < data.length; i++) {
            let item = data[i];
            this.addItem(node, item);
        }
    }

    clear() {
        let items = this.el.querySelectorAll('li[menuitem]');
        for(let item of items) item.remove();
    }
	/**
	 * @typedef {Object} Menu.DataItem
	 * @property {string} label Display text for the item	 
	 * @property {string} [icon] Icon to use for item 	 
	 * @property {string} [severity] 	 
	 * @property {boolean} [collapsed] forces collapsing of the item (only appliable to parents of a sub list)	 
	 * @property {Array<Menu.DataItem>} [items] Array of child items (IMPORTANT: only 1 sub level supported)
	 */

	/**
	 * Adds an entry to the list
	 * @param {DOMElement} target The UL element to paste the entry to
	 * @param {Menu.DataItem} item The entry description
	 */
    addItem(target, item) {
        let node = new MenuItem();
        
        let parent = target;
        let el = node.el;
        parent.appendChild(el);
        parent = el;

        if(item.severity) {
            node.el.setAttribute(item.severity, '');
        }
        
        if(item.route || item.onclick) {
            el = ui.create(`<a href="${item.route || '#'}" title="${item.label.replace(/<\/?[^>]+(>|$)/g, "")}"></a>`);
            if(item.onclick) el.addEventListener('click', item.onclick);
            node.el.classList.add('menu-link');
        } else {
            el = ui.create(`<div class="nolink"></div>`);
        }
        parent.appendChild(el);
        parent = el;

        if(item.icon) { parent.appendChild(ui.create(`<i class="${item.icon}"></i>`)); }

        parent.appendChild(ui.create(`<label>${item.label}</label>`));

        if(item.items) {

            el = ui.create(`<button eicbutton rounded small class="icon-angle-up"></button>`);
            el.addEventListener('click', this.toggle.bind(this));
            parent.appendChild(el);

            el = ui.create(`<ul></ul>`)
            parent.parentElement.appendChild(el);
            this.parse(item.items, el)

			if(item.collapsed) { this.collapse(el); }
        }  
    }
	
	/**
	 * Event handler for collapsed state change
	 * @param {*} event 
	 */
    toggle(event) {
        event.preventDefault();
        event.stopPropagation();

        let list = event.currentTarget.parentElement.parentElement.querySelector('ul');

        if(list.hasAttribute('collapsed'))
            this.expand(list);
        else
            this.collapse(list);
    }

    //get collapsed() { return }
	/**
	 * Collapses a menu item 
	 * @param {*} el 
	*/
    collapse(el) { 
        el.setAttribute('collapsed', ''); 
        let button = el.parentElement.querySelector('button');
        button.classList.remove('arrow-expand');
        // hack for resetting animation
        void button.offsetWidth;
        button.classList.add('arrow-collapse');
    }

	/**
	 * Expands a menu item
	 * @param {*} el 
	 */
    expand(el) {
        el.removeAttribute('collapsed');
        let button = el.parentElement.querySelector('button');
        button.classList.add('arrow-expand');
        void button.offsetWidth;
        button.classList.remove('arrow-collapse');
    }

	/**
	 * Event handler for menu filtering
	 * @param {*} event 
	 */
    filter(event) {
        let query = event.currentTarget.value.toLowerCase();

        let entries = this.el.querySelectorAll(`[menuitem].menu-link`);

        for(let entry of entries) {
            if(query == '') {
                ui.show(entry);
            } else {
                let label = entry.querySelector('a > label').innerText;
                
                if(label.toLowerCase().indexOf(query) != -1)
                    ui.show(entry, 'flex');
                else 
                    ui.hide(entry);
            }

        }
    }
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
class MenuItem extends EicComponent {
    

    constructor(el, options) { 
        let defaultOptions = {
            collapsable: true,
            disabled: false,
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});

        if(!el) { this.el = ui.create(`<li menuitem></li>`) }
    }

    addItem(el) { }
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
class DropDown extends EicComponent {

    constructor(el, options) {
        let defaultOptions = {
            badge: null,
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }      
        super(el, {...defaultOptions, ...(options || {} )});

        if(this.el.querySelector('[eicbutton]'))
            this.button = new Button(this.el.querySelector('button'));
        if(this.el.querySelector('[eicuser]'))
            this.button = new UserIcon(this.el.querySelector('button'));
        this.options.badge = this.button.badge;
        this.button.click = this.toggle.bind(this)
        this.menu = new Menu(this.el.querySelector('menu'));
        this.el.append(this.menu.el);
        this.menu.el.addEventListener('click', this.onMenuClick.bind(this));
    }

    onMenuClick(event) {
        event.preventDefault();
        event.stopPropagation();

        this.collapse();
    }

    toggle(event) {
        event.preventDefault();
        event.stopPropagation();

        this.menu.el.setAttribute('align', this.position.top > (window.innerHeight / 2) ? 'top': 'bottom' );
        this.menu.el.setAttribute('justify', this.position.left > (window.innerWidth / 2) ? 'right': 'left' );

        if(this.el.hasAttribute('expanded'))
            this.collapse();
        else
            this.expand();
    }

    collapse() { this.el.removeAttribute('expanded'); }

    expand() { this.el.setAttribute('expanded', '');}
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
class Input extends EicComponent {
    
    constructor(el, options) {
        let defaultOptions = {
            type: 'text',
            value: '',
            maxlen:null,
            maxbytes:null,
            maxwords:null,
            hint: null
        }

        if(el && (el.getAttribute('data-eicui-id') != null) 
           && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
            return(ui._components[el.getAttribute('data-eicui-id')])
        }
        super(el, {...defaultOptions, ...(options || {} )});
        
        if(!el) el = ui.create(`<input eicinput type="${this.options.type}" value="${this.options.value}" />`);        
        this.coat(el);
    }

    get value() { return this.input.el.value; }

    set value(val) {         
        this.input.el.value = val; 
        this.update();

    }

    set disabled(state) {
        super.disabled = state;
        if(this.badge) {
            if(state == true) {
                ui.hide(this.badge.el);
            } else {
                ui.show(this.badge.el);
            }
        }
        if(this.hint) {
            if(state == true) {
                ui.hide(this.hint);
            } else {
                ui.show(this.hint);
            }
        }
    }
    
    get disabled() { return super.disabled; }

    coat(el) {

        if(el.hasAttribute('maxlen')) this.options.maxlen = el.getAttribute('maxlen');
        if(el.hasAttribute('maxbytes')) this.options.maxbytes = el.getAttribute('maxbytes');
        if(el.hasAttribute('maxwords')) this.options.maxwords = el.getAttribute('maxwords');
        if(el.hasAttribute('hint')) this.options.hint = el.getAttribute('hint');

        if(this.el.classList.contains('required')) {
            this.options.hint = 'This field is required. ' + (this.options.hint ? this.options.hint: ''); 
        }

        this.input = new EicComponent(el);
        this.el = this.input.el;

        this.container = ui.create(`<div class="eicui-input-container"></div>`)
        if(el.parentNode) el.parentNode.insertBefore(this.container, el);

        this.container.append(this.el);

        if(this.options.hint) {
            this.hint = ui.create(`<div class="eicui-input-hint">${this.options.hint}</div>`);
            this.container.append(this.hint);
        }

        if(this.options.maxbytes || this.options.maxwords || this.options.maxlen) {
            this.badge = new Badge();
            this.update();
            this.container.insertBefore(this.badge.el, this.el);
            this.input.addEventListener('keyup', this.onKeyPressed.bind(this));
        }
    }

    update() {
        let remaining = 0;
        let unit = '';

        if(this.options.maxlen) {
            remaining = this.options.maxlen - this.value.length;
            unit = ' chars';
        } else
        if(this.options.maxwords) {
            remaining = this.options.maxwords - this.value.split(' ').filter(e => e != '').length;
            unit = ' words';
            
        } else 
        if(this.options.maxbytes) {
            remaining = this.options.maxbytes - (new TextEncoder().encode(this.value)).length;
            unit = ' bytes';
        }
        if(this.badge) {
            if(remaining < 0) {
                this.badge.value = 'too long'; 
                this.badge.severity = 'danger';
                this.input.severity = 'danger';
                this.input.el.classList.add('invalid');
            } else {
                this.badge.value = remaining + unit; 
                this.badge.severity = 'secondary';
                this.input.severity = 'secondary';
                this.input.el.classList.remove('invalid');
            } 
        }
        
    }

    onKeyPressed(event) { this.update(); }
}

/**
 * @augments Input
 * @category EICUI/Components
 */
class InputHidden extends Input {
    
    constructor(el, options) {
        super(el, options);
        this.container.classList.add('eic-input-hidden')
    }
}

/**
 * @augments Input
 * @category EICUI/Components
 */
class InputSearch extends Input {
    
    _loading = false;

    constructor(el, options) {
        let defaultOptions = {
            type: 'text',
            value: '',
            maxlen:null,
            hint: null,
            placeholder: '',
            allowEmptyString: true,
            allowQueryOnKey: false
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }        
        super(el, {...defaultOptions, ...(options || {} )});

        // wrapping input with icon overlay
        let wrapper = ui.create('<div class="input-wrapper"></div>');
        this.icon = ui.create('<i class="icon-search"></i>');
        this.container.prepend(wrapper);
        wrapper.append(this.icon);
        wrapper.append(this.input.el);
        //this.container.prepend(this.icon);
        this.container.classList.add('eic-input-search');
        this.input.addEventListener('keypress', this.onKeyPressed.bind(this));
    }
    
    onKeyPressed(event) {
        if(this._loading) {
            event.preventDefault();
            event.stopPropagation();
            return;
        }

        if(this.options.allowQueryOnKey || event.keyCode == 13) this.onQuery()
    }

    onQuery() {}

    get loading() { return this._loading; }
    set loading(active) {
        
        this._loading = active;

        if(active) {
            this.icon.className = "icon-spinner spin"
        } else {
            this.icon.className = "icon-search"
        }
    }
}

/**
 * @augments Input
 * @category EICUI/Components
 */
class InputDate extends Input {
    
    constructor(el, options) {
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, options);
    }
}

/**
 * @augments Input
 * @category EICUI/Components
 */
class Checkbox extends Input {
    
    constructor(el, options) {
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }      
        super(el, options);
        
        this.container.classList.add('eic-checkbox');
        if(el.hasAttribute('label'))
            this.container.append(ui.create(`<label>${el.getAttribute('label')}</label>`))
    }
}


/**
 * @augments Input
 * @category EICUI/Components
 */
class InputToggler extends Input {
    
    /**
     * @event onToggle#onBeforeUploadAllStart
     * @type {event}
     * @param {string} value
     * @param {Object} the toggler component
	 */       
    onToggle(value, object) { }


    constructor(el, options) { 
        let defaultOptions = {
            value: false,
            labelLeft:'',
            labelRight:'',
            title:'',
            trueValue: 'yes',
            falseValue: 'no',
            classOn:'',
            classOff:'',
            tabindex:0,
            disabled:false
        }
        options = options || {}
        if(el.hasAttribute('labelright')) options.labelRight = el.getAttribute('labelright');
        if(el.hasAttribute('labelleft')) options.labelLeft = el.getAttribute('labelleft');
        if(el.hasAttribute('classOn')) options.classOn = el.getAttribute('classOn');
        if(el.hasAttribute('classOff')) options.classOff = el.getAttribute('classOff');        
        if(el.hasAttribute('title')) options.title = el.getAttribute('title');
        if(el.hasAttribute('trueValue')) options.trueValue = el.getAttribute('truevalue');
        if(el.hasAttribute('falseValue')) options.falseValue = el.getAttribute('falsevalue');
        if(el.hasAttribute('value')) options.value = el.getAttribute('value');
        if(el.hasAttribute('tabindex')) options.tabindex = el.getAttribute('tabindex');
        if(el.hasAttribute('disabled')) options.disabled = true;

        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }
        super(el, {...defaultOptions, ...options});
        
        if(this.options.value == this.options.trueValue) this.turnOn()
        else this.turnOff()

    }
    
    coat(el) {
        super.coat(el)
        this.container.classList.add('input-toggler')
        el.style.display = 'none'
        this.labelRight = ui.create(`<div class="toggle-label-right">${this.options.labelRight}</div>`)
        this.labelLeft = ui.create(`<div class="toggle-label-left">${this.options.labelLeft}</div>`)
        this.switch = ui.create(`
                <div class="toggle-switch" title="${this.options.title}" ${this.options.disabled ? 'disabled' : ''}>
                    <span class="toggle-bar">
                        <span class="toggle-thumb"></span>
                    </span>
                </div>
        `)
        this.container.appendChild(this.labelLeft)
        this.container.appendChild(this.switch)
        this.container.appendChild(this.labelRight)
        this.container.setAttribute('tabindex',this.options.tabindex)
        ui.copyEicuiStyling(this.el, this.container)
        this.thumb = this.container.querySelector(".toggle-thumb")
        this.switch.addEventListener('click', this.toggle.bind(this))
    }

	/**
	 * The severity attribute for the component 
	 * @type {UISeverity} 
	 */
	set severity(value) {

		if(!this.container) return;

		let values = [ 'primary','secondary','success','warning','danger','accent','info' ];
		values.forEach(item => this.container.removeAttribute(item));

		this.container.setAttribute(value, '');
    }

    turnOn() {
        if(this.options.classOff) this.switch.classList.remove(this.options.classOff)
        if(this.options.classOn) this.switch.classList.add(this.options.classOn)
        this.thumb.classList.add('turned-on')
        this._value = this.options.trueValue
        this.el.value = this._value
        this.onToggle(this._value, this)
        this.container.focus()
    }

    turnOff() {
        if(this.options.classOn) this.switch.classList.remove(this.options.classOn)
        if(this.options.classOff)this.switch.classList.add(this.options.classOff)
        this.thumb.classList.remove('turned-on')
        this._value = this.options.falseValue
        this.el.value = this._value
        this.onToggle(this._value, this)
        this.container.focus()
    }

    toggle(event) {
        if(event) { event.preventDefault(); event.stopPropagation(); }
        if(this.options.disabled) return
        if(this._value == this.options.trueValue) this.turnOff()
        else this.turnOn()
    }

    get value() { return(this._value) }

    set value(v) {
        if(v == this.options.trueValue ) this.turnOn()
        else this.turnOff()
    }

    set disabled(value){
        this.options.disabled = value
        if(value) this.switch.setAttribute('disabled','') // for the css
        else this.switch.removeAttribute('disabled')
    }
}


/**
 * @augments Input
 * @category EICUI/Components
 */
class InputNumber extends Input {
    
    constructor(el, options) {
        let defaultOptions = {
            type: 'number',
            value: '',
            max: null,
            min: null,
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});

        if(el.hasAttribute('min')) this.options.min = parseFloat(el.getAttribute('min'));
        if(el.hasAttribute('max')) this.options.min = parseFloat(el.getAttribute('max'));
        
        this.input.el.addEventListener('blur', this.validate.bind(this));
    }
    
    validate() {
        let valid = true
        if(this.options.min) 
            if(this.input.el.value < this.options.min) valid = false;
        
        if(this.options.max) 
            if(this.input.el.value > this.options.max) valid = false;
        
        if(!valid) {
            this.input.el.classList.add('invalid')
            this.container.classList.add('validation-failed')
        } else {
            this.input.el.classList.remove('invalid')
            this.container.classList.remove('validation-failed')
        } 
        
        if(!valid) 

        return valid;
    }
}

/**
 * @augments Input
 * @category EICUI/Components
 */
class InputCurrency extends Input {
    
    constructor(el, options) {
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, options);

        el.setAttribute('type', 'number');
        this.container.classList.add('input-currency');
        let marker = ui.create(`<i class="currency-marker">€</i>`);
        this.container.insertBefore(marker, el);
    }
}

/**
 * @augments Input
 * @category EICUI/Components
 */
class Textarea extends Input {
    
    constructor(el, options) {
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el || `<textarea eictextarea>${options.value || ''}</textarea>`, options);    
    }
}

/**
 * @augments EicComponent
 * @category EICUI/Components
 */
class Form extends EicComponent {
    
    constructor(el,options) {
        let defaultOptions = {
            mode: 'edit',
            skipValidation: false
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});
    }

    get mode() { return this.options.mode; }
    set mode(value) { this.options.mode = value; }

    validate(extendedReport) {
        let fields = this.scan();
        let globalReport = { valid: true, issues: 0 }

        for(let field of fields) {    
            let report = this.validateField(field);
            field.container.classList.remove('validation-failed');
            if(!report.valid) { 
                field.container.classList.add('validation-failed'); 
                globalReport.issues++;
            }

            globalReport.valid = globalReport.valid && report.valid; 
        }
        
        return extendedReport ? globalReport: globalReport.valid;
    }

    get value() {
        let fields = this.scan();
        let values = {};

        function add(path, value) {
            let segments = path.split('.');
            let field = segments.pop();
            let current = values;
            let re = /\[([^)]+)\]/;
            let isArray = null;

            for(let segment of segments) {
                
                isArray = re.exec(segment);

                if(isArray) {
                    // remove index from segment string
                    segment = segment.replace(isArray[0], '');

                    if(!(current.hasOwnProperty(segment)))
                        current[segment] = [];
                    
                    current = current[segment];

                    // fill array with empty items until length matches selected index
                    while(current.length < (parseInt(isArray[1]) + 1)) current.push({});
                    
                    current = current[parseInt(isArray[1])];
                } else {
                    if(!(current.hasOwnProperty(segment)))
                    current[segment] = {};
                    current = current[segment];
                }
            }
            
            // checking if field is array
            isArray = re.exec(field);

            if(isArray) {
                field = field.replace(isArray[0], '');

                if(!(Array.isArray(current[field]))) current[field] = [];

                // fill array with empty items until length matches selected index
                while(current[field].length < (isArray[0] + 1)) current = current[field].push({});

                current[field][parseInt(isArray[1])] = value;
            } else {
                current[field] = value;
            }
        }

        for(let field of fields) {
            // skip checkboxes either unchecked or without a value 
            //if(field.el.type == 'checkbox' && (field.el.value && !field.el.checked)) continue;

            let value = field.el.type == 'checkbox' ? field.el.checked : field.el.value;

            if(field.el.nodeName == 'SELECT') {
                if(field.el.hasAttribute('multiple')) {
                    // this one is nasty... getting all selected values of a select in an array
                    value = Array.prototype.slice.call(field.el.querySelectorAll('option[selected="selected"], option[selected]'),0).map(function(v,i,a) { return v.value; })
                } else {
                    let option = field.el.querySelector('option[selected="selected"], option[selected]')
                    value = option ? option.value : '';
                }
            }

            switch(field.el.dataset.type) {
                case 'boolean': value = value !== '' ? value === 'true': null; break;
                case 'number': value = isNaN(parseFloat(value)) ? null : parseFloat(value); break;
                case 'ignore': continue;
            }
            add((field.el.dataset.path || '') + (field.el.name ? (field.el.dataset.path ? '.': '') + field.el.name: ''), value);
        }
        
        return values;
    }

    fill(components,data) {
        let fields = this.scan();
        for(let field of fields) {
            let component = this.element2component(components, field.el.name, field.el.dataset.path);
            
            if(component) component.disabled = this.options.mode == 'read' || component.disabled;

            let item = this.findDataField(data, field.el.dataset.path, field.el.name)
            if(item !== null && typeof item !== 'undefined') {
                component.value = item;
            }
        }
    }

    findDataField(data, path, name) {
        let levels = path && path != '' && path !== undefined ? path.split('.'): [];
        let tree = data;
        for(let level of levels) 
            if(tree.hasOwnProperty(level)) tree = tree[level]; 

        if(tree.hasOwnProperty(name)) return tree[name];
    }

    element2component(components, name, path) {
        return (
            components
            .find(o => 
                o.el.hasAttribute('name') && o.el.getAttribute('name') == name &&
                ( !path || (path && o.el.dataset.path == path))
            )
        )
    }

    /**
     * Scans form and returns all avaliable fields
     */
    scan(includeIgnored) { 
        let fields = [];
        let elements = 
            includeIgnored ?
                this.el.querySelectorAll('input,select,textarea'):
                this.el.querySelectorAll('input:not(.ignore),select:not(.ignore),textarea:not(.ignore)');
        
        for(let element of elements) {
            let container = element;
            
            if(
                element.hasAttribute('eicinput') || 
                element.hasAttribute('eiccheckbox') ||
                element.hasAttribute('eictextarea')
            ) { container = element.parentElement; }
            
            if(element.hasAttribute('eicselect')) { container = element.nextSibling; }
            
            fields.push({
                el: element,
                container: container
            });
        }
        return fields; 
    }

    validateField(field) {
        let report = {
            target: field.el,
            valid: true,
            errors: []
        };
        let value = report.target.value;
        let nodeType = report.target.nodeName.toLowerCase();
        let type = 'text';

        switch(nodeType) {
            case 'input':
                type = report.target.getAttribute('type') || 'text';
                break;
            case 'textarea':
                type = 'text';
                break;
            case 'select':
                // workaround for Safari: forcing value based on selected option (el.value sending wrong value)
                let option = field.el.querySelector('option[selected="selected"], option[selected]')
                if(option) value = option.value
                break;
        }

        if(report.target.classList.contains('required')) {
            if(['text', 'email','url', 'date', 'number', 'currency'].includes(type)) {
                let test = (value.length != 0);
                if(!test) report.errors.push('field cannot be empty')
                report.valid = report.valid && test;
            }

            if(type == 'checkbox') {
                report.valid = report.valid && report.target.checked;
            }
        }

        if(['number', 'currency'].includes(type)) {
            let test = (value.length == 0 || (!isNaN(value) && !isNaN(parseFloat(value))));
            if(!test) report.errors.push('field must be numeric')
            report.valid = report.valid && test;

            if(report.target.getAttribute('min')) {
                let test = (
                    value.length == 0 || 
                    (
                        !isNaN(value) && 
                        !isNaN(report.target.getAttribute('min')) && 
                        parseFloat(value) >= parseFloat(report.target.getAttribute('min'))
                    ) 
                    
                );
                if(!test) report.errors.push('lower than min range')
                report.valid = report.valid && test;
            }
        }

        if(report.target.classList.contains('invalid')) {
            report.valid = false;
            report.errors.push('content is invalid')
        }

        return report;
    }
}

/**
 * As a TABLE replacer, the data grid component delivers a flexible way to present data sets.
 * The component features:
 * - sortable columns
 * - filtrable content based on definable column types (freetext, lists, ...)
 * - draggable rows (reordering)
 * - inline row editing
 * - flexible "by row" actions
 * - CCS3 grid based layout providing flexible designs and styling 
 * 
 * @extends EicComponent
 * @category EICUI/AdvancedComponents
 * @version 2.0
 */
class DataGrid extends EicComponent { 

    /**
     * Triggered when a row is clicked
     *
     * @event DataGrid#rowClick
     * @type {mouseevent}
     * @property {element} currentTarget The target grid-row DOM element
     */

     /**
     * Triggered when selection changes
     *
     * @event DataGrid#change
     * @type {event}
     */

    /**
    * @param {element|string} el A DOM element or the selector string for the element which will contain the component
    * @param {object} [options] The grid options
    * @param {array} [options.headers] Defines the grid column headers
    * @param {string} [options.height] Forces the css height property of the component 
    * @param {boolean} [options.editable] The grid options
    * @param {number} [options.maxRows] Sets the maximum amount of rows the content can hold
    * @param {boolean} [options.allowRowDrag] Enables graggable rows
    * @param {boolean} [options.allowRowDrag] Enables graggable rows
    * 
    */
    constructor(el, options) { 
        let defaultOptions = {
            height: -1,
            editable: false,
            maxRows: -1,
            minRows: -1,
            allowRowDrag: false,
            headers: [],
            rowActions: [],
        };
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});
        
        this.totalRows = 0;
        this.activeRows = 0;

        this.setOptions(options);
        this.el.setAttribute('eicdatagrid', '');
        
        this.header = ui.create(`<header><ul class="rows"><li class="row"><div class="cell"><input type="checkbox" /></div></li></ul></header>`);
        this.el.appendChild(this.header);
        
        this.body = ui.create(`<div class="dataset"><ul class="rows"></ul></div>`);
        this.el.appendChild(this.body);

        this.footer = ui.create(`<footer><div><span class="displayed">0</span> rows out of <span class="total">0</span></div></footer>`);
        this.el.appendChild(this.footer);
        
        let headerRow = this.header.querySelector('.row');

        if(this.options.height != -1) {
            this.body.style.maxHeight = this.options.height;
        }

        this.dataset = this.body.querySelector('.rows');
        
        if(this.options.allowRowDrag) {
            this.el.classList.add('indexed');
        }
        
        this.globalSelector = this.header.querySelector('.cell');
        this.globalSelector.addEventListener('click', this.onGlobalClick.bind(this));

        this.filters = [];
        
        for(let i = 0; i < this.options.headers.length; i ++) {
            let column = this.options.headers[i];
            let safeLabel = ui.create(`<div>${column.label}</div>`).innerText
            let cell = ui.create(`<div class="cell"><span title="${safeLabel}">${column.label}</span></div>`)

            if(column.sortable) {            
                let label = cell.querySelector('span');            
                label.setAttribute('data-order', 'asc');
                label.classList.add('sortable');
                if(column.type && (column.type == 'number' || column.type == 'currency')) label.classList.add('numeric');
                label.addEventListener('click', this.onColumnSort.bind(this));
            }

            this.filters.push(null);

            if(column.filter && column.filter == 'text') {
                let input = ui.create('<input type="text" />');
                this.filters[i] = input;
                cell.appendChild(input);
                input.addEventListener('keyup', this.filter.bind(this));
            }

            if(column.filter && column.filter == 'list') {
                let select = ui.create('<select><option></option></select>');
                this.filters[i] = select;
                cell.appendChild(select);
                select.addEventListener('change', this.filter.bind(this));
            }

            this.header.querySelector('.row').appendChild(cell);
        }
        
        this.editing = false;

        let actions = ui.create('<div class="cell actions"></div>');

        this.header.querySelector('.row').appendChild(actions);
        
        if(this.options.editable) {
            this.btNew = ui.create(`<button eicbutton primary>Add</button>`);
            this.btNew.addEventListener('click', this.onRowAdd.bind(this));
            actions.appendChild(this.btNew);
            this.options.rowActions.push({ icon: '<i class="icon-edit"></i>', title: 'Edit item', callback: this.onRowEdit.bind(this) });
            this.options.rowActions.push({ class: 'remove', icon: '<i danger class="icon-bin"></i>', title: 'Delete item', callback: this.onRowDelete.bind(this) });
            this.enableEditing(this.rows().length < this.options.maxRows);
        }
    }

    set loading(value) {
        if(value) 
            this.el.setAttribute('loading', true);
        else 
            this.el.removeAttribute('loading');
    }

    set enableFooter(state) { state ? this.el.removeAttribute('footer'): this.el.setAttribute('footer', 'hidden')}
    set enableHeader(state) { state ? this.el.removeAttribute('header'): this.el.setAttribute('header', 'hidden')}
    
    /**
    * Clears the grid content
    */
    clear() { 
        this.dataset.innerHTML = ''; 
        for(let filter of this.filters) {
            if(filter) filter.value=''; 
        }
        for(let header of this.header.querySelectorAll('.cell span')) {
            header.classList.remove('active') 
        }
        this.activeRows = 0;
        this.totalRows = 0;
        this.updateFooter();
    }
    
    /**
    * Applies column filters to the content
    */
    filter() {
        let rows = this.dataset.querySelectorAll('.row');
        this.activeRows = 0;
        for(let r = 0; r < rows.length; r++) {

            let valid = true;
            let row = rows[r];
            for(let i = 0; i < this.filters.length; i++) { 

                if(this.filters[i]) {
                    let value = this.filters[i].value;
                    
                    if(value != '') {
                        
                        if(this.filters[i].nodeName == 'SELECT' && row.querySelectorAll('.cell')[i+1].innerText != value) valid = false;
                        
                        if(this.filters[i].nodeName == 'INPUT') {
                            var str = value.replace('(', '\\(').replace(')', '\\)');
                            var re = RegExp(str, 'i');
                            if(!re.test(row.querySelectorAll('.cell')[i+1].innerText)) valid = false;
                        } 
                    }         
                }
            }

            if(!valid) {
                this.hideRow(row);
            } else {
                this.showRow(row);
                this.activeRows++;
            }
        }
        this.updateFooter();
        this.onRowFiltered();
    }

    /**
    * checks grid validity: - minRows criteria
    */
    isValid() {
        let valid = true;

        if(this.options.minRows != -1) {
            valid = valid && this.rows().length >= this.options.minRows;
        }

        return valid;
    }

    /**
    * Event handler when grid is filtered. overridable.
    */
    onRowFiltered() { }

    /**
    * Sort the content
    * @param {number} col The column index to sort on
    * @param {string} order Sets the sort order. Either 'asc' or 'desc'.
    * @param {boolean} isNumber If true, cell values will be sorted as numbers. If false or null values will be sorted as strings. 
    */
    sort(col, order, isNumber) {
        let index = [];
        let i = 0;

        let rows = this.rows();
        
        for(i = 0; i < rows.length; i++) { 
            let current = rows[i];
            let value = current.querySelector('.cell:nth-child(' + (col + 1) + ')').innerText.toLowerCase();
          
            if(isNumber) {
                let n = parseFloat(value);
                value = !isNaN(n) ? n: -1;    
            }
            
            index.push({value: value, row: current});
        }
        
        if(order == 'asc') {
            index.sort((a,b) => (a.value > b.value) ? 1 : ((b.value > a.value) ? -1 : 0));
        } else {
            index.sort((a,b) => (a.value < b.value) ? 1 : ((b.value < a.value) ? -1 : 0));
        }
        
        for(i = 0; i < index.length; i++) {
            let item = index[i];
            let row = item.row.parentElement.removeChild(item.row);
            this.dataset.appendChild(row);
        }
    }

    /**
    * Rebuilds the columns filter lists.
    */
    updateFilters() { 
        for(let i = 0; i < this.filters.length; i++) { 
            let filter = this.filters[i];
            if(filter && filter.nodeName == 'SELECT') { 
                filter.innerHTML = '';
                filter.appendChild(ui.create('<option></option>'));
                var values = this.getColValues(i);
                for(let o = 0; o < values.length; o++) { 
                    var val = values[o];
                    filter.appendChild(ui.create(`<option value="${val}">${val}</option>`));
                }
            }
        }
    }

    updateFooter() {
        this.footer.querySelector('.displayed').innerText = this.filteredRows.length;
        this.footer.querySelector('.total').innerText = this.rows().length;
    }
    
    /**
    * @private
    */
    enableEditing(allowEditing) {
        if(allowEditing) {
            this.btNew.classList.remove('disabled');
        } else {
            this.btNew.classList.add('disabled');
        }
    }

    /**
    * Renders the inline edit form
    * @private
    * @param {element} row the targetted grid-row element to be edited
    */
    buildEditor(row) {
        let editor = ui.create(`<div class="row editor" data-id=""><div class="cell"><i class="icon-edit"></div></div></div>`);
        let values = row ? row.querySelectorAll('.cell'): null;

        for(let i = 0; i < this.options.headers.length; i++) {
            let header = this.options.headers[i];
            
            // TODO use header type to generate either inputs or selects
            let cell = ui.create(`<div class="cell"><input type="text" value="${values ? values[i+1].innerHTML.replace(/"/g,'&quot;'): ''}" /></div>`);
            editor.appendChild(cell)
        }

        let btSave = ui.create(`<button eicbutton primary>Save</button>`);
        let btCancel = ui.create(`<button eicbutton secondary>Cancel</button>`);
        
        if(values) {
            btSave.addEventListener('click', this.onRowUpdate.bind(this));
            btCancel.addEventListener('click', this.onRowCancelEdit.bind(this));
        } else {
            btSave.addEventListener('click', this.onRowCreate.bind(this));
            btCancel.addEventListener('click', this.onRowCancelCreate.bind(this));
        }

        let actions =  $(`<div class="cell actions"></div>`);
        actions.appendChild(btCancel);
        actions.appendChild(btSave);
        editor.appendChild(actions);

        return editor;
    }

    /**
    * Gets a grid-row element based on its id
    * @param {string} id the target id
    * @return {element|null} the matched grid-row element 
    */
    getRowById(id) { 
        let rows = this.dataset.querySelectorAll('.row:not(.hidden)');
        for(let row of rows) {
            if(row.getAttribute('data-id') == id) return row;
        }
    }

    /**
    * Gets an array of all distinct values of a column
    * @param {number} num The index of the column to be scanned (starting from 0)
    * @return {array} An array of available values 
    */
    getColValues(col) {
        let values = [];
        this.dataset.querySelectorAll('.cell:nth-child(' + (col + 2) + ')').forEach( function(el) {
            if(values.indexOf(el.innerText) == -1) values.push(el.innerText);
        });
        
        return values.sort();
    }

    /**
    * Hides a row element
    * @param {element} row the target grid-row element 
    */
    hideRow(row) { row.classList.add('hidden'); }

    /**
    * Shows a grid-row element
    * @param {element} row the target grid-row element 
    */
    showRow(row) { row.classList.remove('hidden'); }
    
    /**
    * Shows the inline editor for adding a new row
    */
    newRow() {
        this.editing = true;
        this.dataset.appendChild(this.buildEditor());
    }

    /**
    * Shows the inline editor for editing an existing row
    * @param {element} row the target grid-row element 
    */
    editRow(row) {
        this.editing = true;
        this.editedRow = row;
        this.hideRow(row);
        row.parentNode.insertBefore(this.buildEditor(row), row.nextSibling);
    }

    /**
    * Validates and applies the inline editing
    * @private
    */
    updateRow() {
        let values = this.dataset.querySelectorAll('.row.editor .cell input');
        let cells = this.editedRow.querySelectorAll('.cell');
        
        for(let i = 0; i < values.length; i++) {
            $(values[i]).classList.remove('validation-failed')
            if(values[i].value == '') {
                $(values[i]).classList.add('validation-failed');
                return;
            }
        }

        for(let i = 0; i < values.length; i++) {
            cells[i+1].innerHTML = values[i].value;
        }

        this.editing = false;
        this.showRow(this.editedRow);
        this.dataset.querySelector('.row.editor').remove();
    }

    /**
    * Validates and adds the inline editing
    */
    createRow() {
        let fields = this.dataset.querySelectorAll('.row.editor .cell input');
        let data = [];
        for(let field of fields) {
            field.classList.remove('validation-failed')
            if(field.value != '') {
                data.push(field.value);
            } else {
                field.classList.add('validation-failed');
                return;
            }
        }

        this.addRow('', data);
        this.dataset.querySelector('.row.editor').remove();
        this.enableEditing(this.rows().length < this.options.maxRows);
        this.editing = false;
    }

    /**
    * Adds a row to the content
    * @param {string} id A unique identifier for the row (id value)
    * @param {array} data An array of the column values. Item count must match column amount.
    * @param {boolean} skipUpdate If set to true, avoids updating column filters. 
    *      Consider using skipUpdate when adding a batch of rows, drastically increasing performances.
    *      Column filters can be updated afterwards suing the DataGrid.updateFilters() method.
    * @return {element} The created grid-row element  
    */
    addRow(id, data, skipUpdate) {
        let row = ui.create('<li class="row"></li>');
        row.setAttribute('data-id', typeof id === 'object' ? JSON.stringify(id): id );
        
        if(this.options.allowRowDrag) { 
            row.addEventListener('mousedown', this.onStartDragRow.bind(this));
        }
        // add selection cell
        let cell = ui.create('<div class="cell"><input type="checkbox" /></div>');
        cell.addEventListener('click', this.onCellClick.bind(this));
        row.appendChild(cell);

        for(let i = 0; i < data.length; i++) {
            let cellAttr = '';
            let cellFix = '';
            let cellValue = data[i];
            let cellTitle = ui.create(`<div>${cellValue}</div>`).innerText;

            switch(this.options.headers[i].type) {
                case 'currency': 
                    cellAttr = 'currency';
                    cellFix = `<span style="font-size:0">${cellValue}</span>`;
                    cellValue = ui.format.currency(cellValue);
                    break;
                case 'number': 
                    cellAttr = 'number';
                    cellFix = `<span style="font-size:0">${cellValue}</span>`;
                    if(cellValue != '') cellValue = ui.format.number(cellValue);
                    break;
                case 'date': 
                    cellAttr = 'date';
                    cellFix = `<span style="font-size:0">${cellValue}</span>`
                    if(cellValue != '') cellValue = ui.format.date(cellValue);
                    break;
                case 'dateTime': 
                    cellAttr = 'date';
                    cellFix = `<span style="font-size:0">${cellValue}</span>`
                    if(cellValue != '') cellValue = ui.format.dateTime(cellValue);
                    break;
                case 'time': 
                    cellAttr = 'date';
                    cellFix = `<span style="font-size:0">${cellValue}</span>`
                    if(cellValue != '') cellValue = ui.format.time(cellValue);
                    break;
                case 'markup': 
                    cellTitle='';
                    break;                    
            }
            row.appendChild(ui.create(`<div ${cellAttr} class="cell" title="${cellTitle}">${cellFix}${cellValue}</div`));
        }

        let actions = ui.create(`<div class="cell actions"></div`);

        if(this.options.rowActions && Array.isArray(this.options.rowActions)) {
            for(let action of this.options.rowActions) { 
                let button = ui.create(`<button eicbutton rounded title="${action.title ? action.title: ''}">${action.icon}</button>`);
                button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this));
                actions.appendChild(button);
            }
        }

        row.appendChild(actions);
        row.addEventListener('click', this.onRowClick.bind(this, row));
        this.dataset.appendChild(row);
        
        if(!skipUpdate) this.updateFilters();
        this.updateFooter();

        this.totalRows = this.dataset.querySelectorAll('.row').length;
        this.activeRows = this.totalRows;

        return row;
    }

    /** 
    * Adds a row to the content
    * @param {number} idProperty Name of the id property in data
    * @param {array} data An array of key-value objects.
    * @param {boolean} skipUpdate If set to true, avoids updating column filters. 
    *      Consider using skipUpdate when adding a batch of rows, drastically increasing performances.
    *      Column filters can be updated afterwards suing the DataGrid.updateFilters() method.
    * @return {element} The grid  
    */
    addRows(idProperty, data, skipUpdate) {
        let props = this.options.headers.map(obj=>obj.property);
        let intersect;
        for(let row of data){ 
            intersect = props.reduce((acc, prop) => { acc.push(row[prop]); return(acc); } , []);
            this.addRow(row[idProperty], intersect, skipUpdate) 
        }
        return(this);
    }

    /**
    * Removes a row from the content
    * @param {element} row the target grid-row element 
    * 
    */
    removeRow(row) { row.remove(); }

    /**
    * Selects a row
    * @param {element} row the target grid-row element 
    */
    selectRow(row) { 
        row.classList.add('selected');
        row.querySelector('.cell:first-child input[type=checkbox]').setAttribute('checked', '');
    }

    /**
    * Unselects a row
    * @param {element} row the target grid-row element 
    */
    unselectRow(row) {
        row.classList.remove('selected');
        row.find('.cell:first-child input[type=checkbox]').setAttribute('checked', '');
    }

    /**
    * Selects all visible rows 
    */
    selectAll() {
        let rows = this.dataset.querySelectorAll('.row:not(.hidden)');
        for(let row of rows) this.selectRow(row);
    }

    /**
    * Unselects all rows
    */
    unselectAll() {
        let rows = this.dataset.querySelectorAll('.row:not(.hidden)');
        for(let row of rows) this.unselectRow(row);
    }

    /**
    * Toggles all visible rows (based on first item value)
    */
    toggleSelection() {
        
        let setCheck = true;
        let defined = false;

        for(let row of this.rows) {
            let cb = row.querySelector('input[type=checkbox]');

            if(!row.classList.has('hidden')) {    
                // use first item value a reference
                if(!defined) {
                    defined = true;
                    setCheck = !cb.checked;
                }
                cb.checked = setCheck;
            } else {
                cb.checked = false;
            }

        }
    }

    /**
    * Gets all grid-row elements
    * @return {array} An array of grid-row elements
    */
    rows() { return this.dataset.querySelectorAll('.row'); }

    /**
    * Gets visible grid-row elements
    * @return {array} An array of grid-row elements
    */
    get filteredRows() { return this.dataset.querySelectorAll('.row:not(.hidden)'); }

    /**
    * Row click event handler default callback. 
    * Override this method with your own row click handler if needed.
    * @param {event} event (callback event) The emitted mouse click event.  
    * @param {element} event.currentTarget The target grid-row element.
    */
    onRowClick(event) { }

    onRowSelect(row) { }

    /**
    * @private
    */
    onRowEdit(event) {
        event.preventDefault();
        event.stopPropagation();
        
        if(!this.editing) {
            this.editRow(ui.queryParent(event.target, '.row'));
        }
    }

    /**
    * @private
    */
    onRowUpdate(event) {
        event.preventDefault();
        event.stopPropagation();

        this.updateRow();
    }

    /**
    * @private
    */
    onRowCreate(event) {
        event.preventDefault();
        event.stopPropagation();

        this.createRow();
    }

    /**
    * @private
    */
    onRowAdd(event) {
        event.preventDefault();
        event.stopPropagation();

        if(!this.btNew.classList.contains('disabled') && !this.editing) this.newRow();
    }

    /**
    * @private
    */
    onRowCancelEdit(event) {
        event.preventDefault();
        event.stopPropagation();
        
        this.editing = false;

        let row = ui.queryParent(event.target, '.row');
        row.parentElement.removeChild(row);
        this.showRow(this.editedRow);
        this.editedRow = null;
    }

    /**
    * @private
    */
    onRowCancelCreate(event) {
        event.preventDefault();
        event.stopPropagation();
        
        this.editing = false;
        let row = ui.queryParent(event.target, '.row');
        row.parentElement.removeChild(row);
    }

    /**
    * @private
    */
    onRowDelete(event) {
        event.preventDefault();
        event.stopPropagation();
        
        if(this.editing) return;

        let row = ui.queryParent(event.target, '.row');
        row.parentElement.removeChild(row);
        this.enableEditing(this.rows().length < this.options.maxRows);
    }

    /**
    * @private
    */
    onCellClick(event) {
        event.stopPropagation();
        
        if(this.editing) {
            event.preventDefault();
            return;
        } 
        
        let row = ui.queryParent(event.target, '.row');

        if(event.target.checked) { 
            this.selectRow(row);
            this.onRowSelect(row);
        } else { 
            this.unselectRow(row);
        }

        this.el.dispatchEvent(new Event('change'));
    }

    /**
    * @private
    */
    onColumnSort(event) {
        
        if(this.editing) return;
        
        let target = event.target.nodeName == 'SPAN' ? event.target: ui.queryParent(event.target, 'span');
        let index = ui.childIndex(target.parentNode);
        
        let order = target.getAttribute('data-order');
        let number = target.classList.contains('numeric');
        
        this.header.querySelectorAll('.cell > span').forEach(function(el) { el.classList.remove('active') }) ;
        target.setAttribute('data-order', order == 'asc'? 'desc': 'asc' );
        target.classList.add('active');

        this.sort(index, order, number);
    }

    /**
    * @private
    */
    onGlobalClick(event) { 
        event.stopPropagation();
        
        if(this.editing) return;
        
        if(event.target.checked) { 
            this.selectAll();
        } else { 
            this.unselectAll();
        }
    }

    /**
    * @private
    */
    onStartDragRow(e) {
        e.preventDefault();

        if(this.editing) {
            e.preventDefault();
            return;
        } 
        
        if(e.target.nodeName == 'A') return;
        if(e.target.nodeName == 'I' && e.target.parentElement.nodeName == 'A') return;
        if(e.target.nodeName == 'INPUT') return;
        
        if(!this.rowInsert) {
            this.rowInsert = ui.create('<div class="row insert"></div>');
            ui.hide(this.rowInsert);
        }

        let row = e.currentTarget;
        
        this.rowDrag = { row: row, pos: 0 };
        
        this.rowDrag.row.style.position = 'fixed';
        this.rowDrag.row.style.top = e.clientY;
        this.rowDrag.row.style.left = e.clientX + 10;
        
        document.addEventListener('mousemove',this.onMoveRow.bind(this));
        document.addEventListener('mouseup',this.onEndDragRow.bind(this));
    }

    /**
    * @private
    */
    onMoveRow(e) {
        if(this.editing) return;
        
        this.rowDrag.row.style.top = e.clientY;
        this.rowDrag.row.style.left = e.clientX + 10;

        ui.hide(this.rowInsert);

        // check if target is row
        if(e.target.classList.has('cell')) { 
            let target = e.target.parentElement;
            
            let h = e.target.clientHeight;
            this.rowDrag.pos = e.offsetY;

            ui.show(this.rowInsert);
            
            if(this.rowDrag.pos > h / 2) {
                this.rowInsert.insertAfter(target)
            } else { 
                this.rowInsert.insertBefore(target)
            }
        }
    }

    /**
    * @private
    */
    onEndDragRow(e) {
        if(this.editing) return;
        
        $(document).off('mousemove');
        $(document).off('mouseup');
        
        this.rowDrag.row.css({
            'position': 'relative',
            'top': 'auto', 'left': 'auto'    
        });

        this.rowInsert.hide();

        // check if target is row
        if(e.target.classList.has('cell')) { 
            let target = $(e.target).parent();
            let h = e.target.clientHeight;
            
            if(this.rowDrag.pos > h / 2) {
                this.rowDrag.row.insertAfter(target);
                this.el.trigger('moved',  [this.rowDrag.row.getAttribute('data-id'), 'after', target.getAttribute('data-id')])
            } else { 
                this.rowDrag.row.insertBefore(target);
                this.el.trigger('moved', [this.rowDrag.row.getAttribute('data-id'), 'before', target.getAttribute('data-id')])
            }
        } 
    }
    
    /**
    *
    */
    on(type, callback) { this.el.on(type, callback); }
}

class UserIcon extends EicComponent {

    _online = false;

    constructor(el, options) {
        let defaultOptions = {
            uuid: '',
            fullname: '',
            online: false,
            severity: '',
            showStatus: false
        }
        if(el && (el.getAttribute('data-eicui-id') != null) 
            && (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
                    return(ui._components[el.getAttribute('data-eicui-id')])
        }       
        super(el, {...defaultOptions, ...(options || {} )});
        this.coat();
    }

    coat() {
        if(!this.el) {
            let capitals = this.options.fullname.split(' ');
            if(capitals.length > 0) {
                this.el = ui.create(`<button eicuser><span></span></button>`);
                this.showStatus = this.options.showStatus;
                this.online = this.options.online;
                this.uuid = this.options.uuid;
                this.fullname = this.options.fullname;
            }
        }

        if(this.options && this.options.onclick) this.click = options.onclick;    

        this.el.addEventListener('click', this.onClick.bind(this));

    }

    get uuid() { return this.options.uuid; }
    set uuid(value) { 
        this.options.uuid = value;
        this.el.dataset.uuid = value; 
    }

    get fullname() { return this.options.fullname; }
    set fullname(value) { 
        let capitals = value.split(' ');
        this.label = ''
        if(capitals.length > 0) {
            this.label = capitals[0][0] + (capitals.length > 1 ? capitals[1][0]: capitals[0][1]);
            this.label = this.label.toUpperCase();
        }
        this.el.setAttribute('title', value);
        this.el.querySelector('span').innerHTML = this.label;
        this.options.fullname = value;
    }

    set showStatus(value) {
        if(value) {
            if(!this.badge) {
                this.badge = new Badge(null);
                this.el.append(this.badge.el);
            }
            this.badge.show()
        } else {
            //this.badge.hide()
        }
    }

    get online() { return this._online; }
    set online(status) { 
        this._online = status; 
        if(this.badge) {
            this.badge.value = `<i class="${status ? 'icon-check': 'icon-cancel'}"></i>`
        }
        if(this._online) {
            this.el.classList.add('online')
        } else {
            this.el.classList.remove('online')
        }
    }

    onClick(event) {
        event.stopPropagation();
        event.preventDefault();

        this.click(event);
    }
    
    click() {}
}

// Array.includes polyfill
if(!Array.prototype.includes) {
    Array.prototype.includes = function(search){
     return !!~this.indexOf(search);
   }
 }
