/**
 * Utility class
 */
export class Util {

    /**
     * @param builder
     */
    constructor(builder) {
        this.builder = builder;
    }

    /**
     * @returns {Element}
     */
    cellSelected() {
        return document.querySelector('.cell-active');
    }

    /**
     * @returns {Element}
     */
    builderStuff() {
        return document.querySelector('#_cbhtml');
    }

    /**
     * @param cell
     * @returns {null|Element}
     */
    cellNext(cell) {
        const dom = new Dom();
        let c = cell.nextElementSibling;

        if (c) {
            if (!dom.hasClass(c, 'is-row-tool') && !dom.hasClass(c, 'is-rowadd-tool')) {
                return c;
            } else {
                return null;
            }
        }
        return null;
    }

    /**
     * @param s
     * @returns {*}
     */
    out(s) {
        if (this.builder) {
            let val = this.builder.opts.lang[s];
            if (val) return val;
            else {
                if (this.builder.checkLang) console.log(s);
                return s;
            }
        } else {
            return s;
        }
    }

    /**
     * @param message
     * @param callback
     */
    confirm(message, callback) {

        const dom = new Dom();
        let html  = `
            <div class="is-modal is-confirm">
                <div style="max-width:526px;text-align:center;">
                    <p>${message}</p>
                    <button title="${this.out('Delete')}" class="input-ok classic">${this.out('Delete')}</button>
                </div>
            </div>
        `;

        const builderStuff = this.builderStuff();
        let confirmmodal   = builderStuff.querySelector('.is-confirm');
        if (!confirmmodal) {
            dom.appendHtml(builderStuff, html);
            confirmmodal = builderStuff.querySelector('.is-confirm');
        }

        this.showModal(confirmmodal, false, () => {

            //this function runs when overlay is clicked. Remove modal.
            confirmmodal.parentNode.removeChild(confirmmodal);

            //do task
            callback(false);

        }, true);

        let buttonok = confirmmodal.querySelector('.is-confirm .input-ok');
        dom.addEventListener(buttonok, 'click', () => {

            this.hideModal(confirmmodal);
            confirmmodal.parentNode.removeChild(confirmmodal); //remove modal

            //do task
            callback(true);
        });
    }

    /**
     * if overlayStay = false, cancelCallback will be called if overlay is clicked.
     - hideModal will remove the modal element, so calling show modal multiple times won't attach multiple events (safe).
     * @param modal
     * @param overlayStay
     * @param cancelCallback
     * @param animated
     */
    showModal(modal, overlayStay, cancelCallback, animated) {
        const dom = new Dom();
        dom.addClass(modal, 'active');

        let animate = false;
        if (this.builder) {
            if (this.builder.opts.animateModal) {
                animate = true;
                if (!animated) { // if not set or false
                    animate = false; // overide
                }
            }
        } else {
            if (animated) { // if set true
                animate = true; // overide
            }
        }

        if (animate) {
            if (this.builder) {
                const buildercontainers = document.querySelectorAll(this.builder.opts.container);
                Array.prototype.forEach.call(buildercontainers, (buildercontainer) => {
                    buildercontainer.style.transform       = 'scale(0.98)';
                    buildercontainer.style.WebkitTransform = 'scale(0.98)';
                    buildercontainer.style.MozTransform    = 'scale(0.98)';
                });
            }
        }

        if (!modal.querySelector('.is-modal-overlay')) {

            let html;
            if (overlayStay) {
                html = '<div class="is-modal-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,0.3);z-index:-1;"></div>';
            } else {
                html = '<div class="is-modal-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,0.000001);z-index:-1;"></div>';
            }

            modal.insertAdjacentHTML('afterbegin', html);

            if (!overlayStay) {
                let overlay = modal.querySelector('.is-modal-overlay');
                dom.addEventListener(overlay, 'click', () => {

                    //cancelCallback
                    if (cancelCallback) cancelCallback();

                    this.hideModal(modal);
                });
            }
        }
    }

    /**
     * @param modal
     */
    hideModal(modal) {
        if (this.builder) {
            const buildercontainers = document.querySelectorAll(this.builder.opts.container);
            Array.prototype.forEach.call(buildercontainers, (buildercontainer) => {
                buildercontainer.style.transform       = '';
                buildercontainer.style.WebkitTransform = '';
                buildercontainer.style.MozTransform    = '';
            });
        }

        const dom = new Dom();
        dom.removeClass(modal, 'active');
    }

    /**
     * @param row
     */
    fixLayout(row) {
        const dom = new Dom();

        const cellCount = row.childElementCount - 2; //minus is-row-tool & is-rowadd-tool

        const rowClass = this.builder.opts.row;
        const colClass = this.builder.opts.cols;
        const colEqual = this.builder.opts.colequal;

        if (colEqual.length > 0) {

            const cols = dom.elementChildren(row);
            cols.forEach((col) => {

                if (dom.hasClass(col, 'is-row-tool') || dom.hasClass(col, 'is-rowadd-tool')) return;

                for (let i = 0; i <= colClass.length - 1; i++) {
                    dom.removeClass(col, colClass[i]);
                }

                for (let i = 0; i <= colEqual.length - 1; i++) {
                    if (colEqual[i].length === cellCount) {
                        dom.addClass(col, colEqual[i][0]);
                        break;
                    }
                }

                if (cellCount === 1) {
                    dom.addClass(col, colClass[colClass.length - 1]);
                }

            });

            return;
        }

        //others (12 columns grid)
        if (rowClass !== '' && colClass.length > 0) {
            let n = 0;

            const cols = dom.elementChildren(row);
            cols.forEach((col) => {

                if (dom.hasClass(col, 'is-row-tool') || dom.hasClass(col, 'is-rowadd-tool')) return;

                n++;
                for (var i = 0; i <= colClass.length - 1; i++) {
                    dom.removeClass(col, colClass[i]);
                }

                if (cellCount === 1) dom.addClass(col, colClass[11]);
                if (cellCount === 2) dom.addClass(col, colClass[5]);
                if (cellCount === 3) dom.addClass(col, colClass[3]);
                if (cellCount === 4) dom.addClass(col, colClass[2]);
                if (cellCount === 5) { // 2, 2, 2, 2, 4
                    if (n === 5) dom.addClass(col, colClass[3]);
                    else dom.addClass(col, colClass[1]);
                }
                if (cellCount === 6) dom.addClass(col, colClass[1]); // 2, 2, 2, 2, 2, 2


                if (cellCount === 7) { // 2, 2, 2, 2, 2, 1, 1
                    if (n >= 6) dom.addClass(col, colClass[0]);
                    else dom.addClass(col, colClass[1]);
                }
                if (cellCount === 8) { // 2, 2, 2, 2, 1, 1, 1, 1
                    if (n >= 5) dom.addClass(col, colClass[0]);
                    else dom.addClass(col, colClass[1]);
                }
                if (cellCount === 9) { // 2, 2, 2, 1, 1, 1, 1, 1, 1
                    if (n >= 4) dom.addClass(col, colClass[0]);
                    else dom.addClass(col, colClass[1]);
                }
                if (cellCount === 10) { // 2, 2, 1, 1, 1, 1, 1, 1, 1, 1
                    if (n >= 3) dom.addClass(col, colClass[0]);
                    else dom.addClass(col, colClass[1]);
                }
                if (cellCount === 11) { // 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
                    if (n >= 2) dom.addClass(col, colClass[0]);
                    else dom.addClass(col, colClass[1]);
                }
                if (cellCount === 12) dom.addClass(col, colClass[0]); // 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1

            });

        }

    }

    /**
     * @param html
     * @param mode
     * @param attr
     * @returns {boolean}
     */
    addContent(html, mode, attr) {
        const dom = new Dom();

        const cell = this.cellSelected();
        let row;
        if (!cell) {
            // If no active cell, check if it is from .row-add-initial (empty info)
            row = document.querySelector('.row-active');
            if (!row) return;
            else {
                // Empty content will always use 'row' mode to insert block.
                mode = 'row';
            }
        } else {
            row = cell.parentNode;
        }

        if (mode === 'cell' || mode === 'cell-left' || mode === 'cell-right') {

            let maxCols = 4;
            if (this.builder.maxColumns) {
                maxCols = this.builder.maxColumns;
            }
            //Limit up to 4 cells in a row
            if (row.childElementCount >= maxCols + 2) { //+2 => includes is-row-tool & is-rowadd-tool
                alert(this.out('You have reached the maximum number of columns'));
                return false;
            }

            this.builder.uo.saveForUndo();

            let cellElement;

            if (this.builder.opts.row === '') {

                // TODO: Test using in old Insite
                let s   = this.builder.opts.cellFormat;
                let pos = s.indexOf('</');
                html    = s.substring(0, pos) + html + s.substring(pos);

                cellElement = this.createElementFromHTML(html);

            } else {

                cellElement = cell.cloneNode(true);

                // Cleanup from module related clone
                cellElement.removeAttribute('data-noedit');
                cellElement.removeAttribute('data-protected');
                cellElement.removeAttribute('data-module');
                cellElement.removeAttribute('data-module-desc');
                cellElement.removeAttribute('data-dialog-width');
                cellElement.removeAttribute('data-html');
                cellElement.removeAttribute('data-settings');
                for (let i = 1; i <= 20; i++) {
                    cellElement.removeAttribute('data-html-' + i);
                }
                cellElement.removeAttribute('data-noedit');

                dom.removeClass(cellElement, 'cell-active');
                cellElement.removeAttribute('data-click');

                if (attr) {
                    cellElement.setAttribute(attr, '');
                }

                cellElement.innerHTML = html;

            }

            row.insertBefore(cellElement, cell);
            if (mode === 'cell' || mode === 'cell-right') {
                dom.moveAfter(cellElement, cell);
            }

            this.builder.applyBehavior();

            this.fixLayout(row);

            cellElement.click(); //change active block to the newly created

        }

        if (mode === 'row') {

            this.builder.uo.saveForUndo();

            let rowElement, cellElement;

            if (this.builder.opts.row === '') {
                rowElement = this.htmlToElement(this.builder.opts.rowFormat);

                let s   = this.builder.opts.cellFormat;
                let pos = s.indexOf('</');
                html    = s.substring(0, pos) + html + s.substring(pos);

                // go to last deeper level
                let targetrow = dom.elementChildren(rowElement);
                while (targetrow.length > 0) {
                    targetrow = targetrow[0];
                    if (dom.elementChildren(targetrow).length > 0) {
                        targetrow = dom.elementChildren(targetrow);
                    } else {
                        break;
                    }
                }
                targetrow.innerHTML = html;

                cellElement = targetrow.firstChild;
                if (attr) {
                    cellElement.setAttribute(attr, '');
                }

            } else {

                cellElement = dom.createElement('div');

                dom.addClass(cellElement, this.builder.opts.cols[this.builder.opts.cols.length - 1]);
                cellElement.innerHTML = html;

                if (attr) {
                    cellElement.setAttribute(attr, '');
                }

                rowElement = dom.createElement('div');
                dom.addClass(rowElement, this.builder.opts.row);
                dom.appendChild(rowElement, cellElement);

            }

            row.parentNode.insertBefore(rowElement, row);
            dom.moveAfter(rowElement, row);

            this.builder.applyBehavior();

            cellElement.click(); //change active block to the newly created

        }

        if (mode === 'elm') {

            let elm = this.builder.activeElement; // See elementtool.js line 195-196. // document.querySelector('.elm-active');
            if (!elm) return;

            this.builder.uo.saveForUndo();

            let element = elm;
            // while(!dom.hasClass(element.parentNode, 'cell-active')) {
            //     element = element.parentNode;
            // }
            element.insertAdjacentHTML('afterend', html);

            this.builder.applyBehavior();

            let newelement = element.nextElementSibling;

            if (newelement.tagName.toLowerCase() === 'img') {
                var timeoutId;
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => {
                    if (newelement.complete) {
                        newelement.click();
                        //console.log(2);
                    }
                }, 200);
            } else {
                newelement.click();
            }


            // LATER: auto scroll

            // LATER: If image, then it needs time to load (resulting incorrect position), so hide element tool.

        }

        // Call onChange
        this.builder.opts.onChange();

    }

    /**
     * https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro
     * @param html
     * @returns {ChildNode}
     */
    htmlToElement(html) {
        var template       = document.createElement('template');
        html               = html.trim(); // Never return a text node of whitespace as the result
        template.innerHTML = html;
        return template.content.firstChild;
    }

    /**
     * @param html
     * @param bSnippet
     * @param noedit
     */
    addSnippet(html, bSnippet, noedit) {

        this.builder.uo.saveForUndo();

        const dom    = new Dom();
        let rowElement;
        let bAddLast = false;
        let cell;
        let cellElement;
        let columnTool;

        const builderStuff = this.builderStuff();
        let quickadd       = builderStuff.querySelector('.quickadd');
        const mode         = quickadd.getAttribute('data-mode');

        if (bSnippet && (mode === 'cell' || mode === 'cell-left' || mode === 'cell-right')) {

            if (noedit) {
                this.addContent(html, mode, 'data-noedit');
            } else {
                this.addContent(html, mode);
            }
            return;

        } else if (bSnippet && mode === 'row') {

            /*
            Buttons, line, social, video, map (Grid layout not included).
            Can be inserted after current row, cell, element, or last row.
            */

            // NEW: See contentbuilder.js line 328
            // OLD: See contentbuilder-jquery.js addSnippet() line 16529

            // Just snippet (without row/column grid), ex. buttons, line, social, video, map.
            // Can be inserted after current row, column (cell), element, or last row.

            // html = `<div class="${this.builder.opts.row}"><div class="${this.builder.opts.cols[this.builder.opts.cols.length-1]}"${(noedit? ' data-noedit': '')}>${html}</div></div>`;
            // OR like addContent() in util.js line 245)

            cellElement           = document.createElement('div');
            cellElement.className = this.builder.opts.cols[this.builder.opts.cols.length - 1];
            cellElement.innerHTML = html;
            if (noedit) {
                cellElement.setAttribute('data-noedit', '');
            }

            rowElement           = document.createElement('div');
            rowElement.className = this.builder.opts.row;
            rowElement.appendChild(cellElement);

            // Add after selected row
            cell = this.builder.cellSelected();
            let row;
            if (cell) {
                row = cell.parentNode;
            } else {
                // If no active cell, check if it is from .row-add-initial (empty info)
                row = document.querySelector('.row-active');
                if (!row) {
                    bAddLast = true;
                }
            }
            // Add after last row
            if (bAddLast) {
                const nodes   = document.querySelectorAll('.is-builder');
                const last    = nodes[nodes.length - 1];
                const rows    = dom.elementChildren(last);
                const lastrow = rows[rows.length - 1];
                row           = lastrow;
            }

            row.parentNode.insertBefore(rowElement, row);
            dom.moveAfter(rowElement, row);

            this.builder.applyBehavior();

            cellElement.click(); //change active block to the newly created

            // Change to row selection
            rowElement.className = rowElement.className.replace('row-outline', '');
            // columnTool = parent.document.querySelector('.is-column-tool');
            columnTool           = document.querySelector('.is-column-tool');
            columnTool.className = columnTool.className.replace('active', '');

        } else {

            /*
            Complete with grid layout. Also may containes custom script(data-html)
            Can be inserted after current row or last row.
            */

            // NEW: See contentbuilder.js line 341 AND contentbuilder-jquery.js (addContentMore) line 11526
            // OLD: See contentbuilder-jquery.js (addContentMore) line 11526

            // Snippet is wrapped in row/colum (may contain custom code or has [data-html] attribute)
            // Can only be inserted after current row or last row (not on column or element).

            var snippet       = document.createElement('div');
            snippet.innerHTML = html;
            var blocks        = snippet.querySelectorAll('[data-html]');
            Array.prototype.forEach.call(blocks, (block) => {

                // Render custom code block
                html = decodeURIComponent(block.getAttribute('data-html'));
                html = html.replace(/{id}/g, this.makeId());
                for (var i = 1; i <= 20; i++) {
                    html = html.replace('[%HTML' + i + '%]', (block.getAttribute('data-html-' + i) === undefined ? '' : decodeURIComponent(block.getAttribute('data-html-' + i))));//render editable area
                }
                block.innerHTML = html;

            });

            //html = snippet.innerHTML;

            // Add after selected row
            cell = this.builder.activeCol;
            let row;
            if (cell) {
                row = cell.parentNode; // in email mode, cell active is also under row active (incorrect, but cell active is not needed in email mode. So this line works!)
            } else {
                // If no active cell, check if it is from .row-add-initial (empty info)
                row = document.querySelector('.row-active');
                if (!row) {
                    bAddLast = true;
                }
            }
            // Add after last row
            if (bAddLast) {
                const nodes   = document.querySelectorAll('.is-builder');
                const last    = nodes[nodes.length - 1];
                const rows    = dom.elementChildren(last);
                const lastrow = rows[rows.length - 1];
                row           = lastrow;
            }

            // Use createContextualFragment() to make embedded script executable
            // https://ghinda.net/article/script-tags/
            var range = document.createRange();
            row.parentNode.insertBefore(range.createContextualFragment(snippet.innerHTML), row.nextSibling);
            rowElement = snippet.childNodes[0];

            // Auto scroll
            const y = row.getBoundingClientRect().top + row.offsetHeight + window.pageYOffset - 120;
            window.scroll({
                top      : y,
                behavior : 'smooth'
            });
            // window.scrollTo(0, y);

            rowElement = row.nextElementSibling; // a must. Must be before applyBehavior() to prevent element delete during fixLayout

            this.builder.applyBehavior(); // checkEmpty & onRender called here

            cellElement = rowElement.childNodes[0];
            cellElement.click(); //change active block to the newly created

            // Change to row selection
            rowElement.className = rowElement.className.replace('row-outline', '');
            // columnTool = parent.document.querySelector('.is-column-tool');
            columnTool           = document.querySelector('.is-column-tool');
            columnTool.className = columnTool.className.replace('active', '');

        }

        // Call onChange
        this.builder.opts.onChange();

    }

    /**
     *
     */
    clearActiveCell() {

        // this.builder.lastActiveCol = this.cellSelected(); // get active cell before cleared (will be used by snippets dialog)

        const dom = new Dom();

        let divs = document.getElementsByClassName('cell-active');
        while (divs.length) divs[0].classList.remove('cell-active');

        divs = document.getElementsByClassName('row-outline');
        while (divs.length) divs[0].classList.remove('row-outline');

        divs = document.getElementsByClassName('row-active');
        while (divs.length) divs[0].classList.remove('row-active');

        divs = document.getElementsByClassName('builder-active');
        while (divs.length) divs[0].classList.remove('builder-active');

        const builderStuff = this.builderStuff();
        if (builderStuff) {
            let columnTool = builderStuff.querySelector('.is-column-tool');
            dom.removeClass(columnTool, 'active');
            let elmTool           = builderStuff.querySelector('.is-element-tool');
            elmTool.style.display = '';
        }

        this.builder.activeCol = null;
    }

    /**
     *
     */
    clearAfterUndoRedo() {
        const dom = new Dom();

        const builderStuff = this.builderStuff();
        let tools          = builderStuff.querySelectorAll('.is-tool');
        Array.prototype.forEach.call(tools, (tool) => {
            tool.style.display = '';
        });

        // If this is a moveable element
        if (this.builder.moveable) {
            this.builder.moveable.updateRect();
        }
        let controlBox = document.querySelector('.moveable-control-box');
        if (controlBox) {
            controlBox.style.display = 'none';
        }

        this.builder.activeSpacer    = null;
        this.builder.activeCodeBlock = null;
        this.builder.activeLink      = null;
        this.builder.activeIframe    = null;
        this.builder.activeTd        = null;
        this.builder.activeTable     = null;
        this.builder.activeModule    = null;

        const icons = document.querySelectorAll('.icon-active');
        Array.prototype.forEach.call(icons, (icon) => {
            dom.removeClass(icon, 'icon-active');
        });
        this.builder.activeIcon = null;

        // RTE
        let rteTool    = builderStuff.querySelector('.is-rte-tool');
        // rteTool.style.display = 'none';
        let rteButtons = rteTool.querySelectorAll('button');
        Array.prototype.forEach.call(rteButtons, (rteButton) => {
            dom.removeClass(rteButton, 'on');
        });

        let elementRteTool = builderStuff.querySelector('.is-elementrte-tool');
        // rteTool.style.display = 'none';
        rteButtons         = elementRteTool.querySelectorAll('button');
        Array.prototype.forEach.call(rteButtons, (rteButton) => {
            dom.removeClass(rteButton, 'on');
        });

        let pops = builderStuff.querySelectorAll('.is-pop');
        Array.prototype.forEach.call(pops, (pop) => {
            pop.style.display = '';
        });
    }

    /**
     *
     */
    hideControls() {

        const builderStuff = this.builderStuff();
        let tools          = builderStuff.querySelectorAll('.is-tool');
        Array.prototype.forEach.call(tools, (tool) => {
            tool.style.display = '';
        });

        // If this is a moveable element
        if (this.builder.moveable) {
            this.builder.moveable.updateRect();
        }
        let controlBox = document.querySelector('.moveable-control-box');
        if (controlBox) {
            controlBox.style.display = 'none';
        }
    }

    /**
     *
     */
    clearControls() {
        const dom = new Dom();

        const builderStuff = this.builderStuff();
        if (!builderStuff) return; // in case the builder is destroyed

        let tools = builderStuff.querySelectorAll('.is-tool');
        Array.prototype.forEach.call(tools, (tool) => {
            tool.style.display = '';
        });

        // If this is a moveable element
        if (this.builder.moveable) {
            this.builder.moveable.updateRect();
        }

        let controlBox = document.querySelector('.moveable-control-box');
        if (controlBox) {
            controlBox.style.display = 'none';
        }

        this.builder.activeSpacer    = null;
        this.builder.activeCodeBlock = null;
        this.builder.activeLink      = null;
        this.builder.activeIframe    = null;
        this.builder.activeTd        = null;
        this.builder.activeTable     = null;
        this.builder.activeModule    = null;
        this.builder.activeImage     = null;

        const icons = document.querySelectorAll('.icon-active');
        Array.prototype.forEach.call(icons, (icon) => {
            dom.removeClass(icon, 'icon-active');
        });
        this.builder.activeIcon = null;

        // show iframe overlay to make it clickable
        let ovls = document.querySelectorAll('.ovl');
        Array.prototype.forEach.call(ovls, (ovl) => {
            ovl.style.display = 'block';
        });

        // Element Panel & Snippets sidebar
        var panels = builderStuff.querySelectorAll('.is-side.elementstyles');
        Array.prototype.forEach.call(panels, (panel) => {
            dom.removeClass(panel, 'active');
        });

        // Element Panel things
        let elms = document.querySelectorAll('[data-saveforundo]');
        Array.prototype.forEach.call(elms, (elm) => {
            elm.removeAttribute('data-saveforundo');
        });

        elms = document.querySelectorAll('.elm-inspected');
        Array.prototype.forEach.call(elms, (elm) => {
            dom.removeClass(elm, 'elm-inspected');
        });

        // RTE
        let rtetool                  = builderStuff.querySelector('.is-rte-tool');
        rtetool.style.display        = 'none';
        let elementRtetool           = builderStuff.querySelector('.is-elementrte-tool');
        elementRtetool.style.display = 'none';

        // Element
        elms = document.querySelectorAll('.elm-active');
        Array.prototype.forEach.call(elms, (elm) => {
            dom.removeClass(elm, 'elm-active');
        });
    }

    /**
     * source: http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript
     */
    makeId() {
        let text     = '';
        let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
        for (let i = 0; i < 2; i++)
            text += possible.charAt(Math.floor(Math.random() * possible.length));

        let text2     = '';
        let possible2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        for (let i = 0; i < 5; i++)
            text2 += possible2.charAt(Math.floor(Math.random() * possible2.length));

        return text + text2;
    }

    /**
     * source: http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
     */
    saveSelection() {
        if (window.getSelection) {
            let sel = window.getSelection();
            if (sel.getRangeAt && sel.rangeCount) {
                let ranges = [];
                for (let i = 0, len = sel.rangeCount; i < len; ++i) {
                    ranges.push(sel.getRangeAt(i));
                }

                this.builder.selection = ranges;

                return ranges;
            }
        } else if (document.selection && document.selection.createRange) {

            this.builder.selection = document.selection.createRange();

            return document.selection.createRange();
        }

        this.builder.selection = null;

        return null;
    }

    /**
     *
     */
    restoreSelection() {
        let savedSel = this.builder.selection;
        if (savedSel) {
            if (window.getSelection) {
                let sel = window.getSelection();
                // sel.removeAllRanges();
                if (document.body.createTextRange) { // All IE but Edge
                    var range = document.body.createTextRange();
                    range.collapse();
                    range.select();
                } else if (window.getSelection) {
                    if (window.getSelection().empty) {
                        window.getSelection().empty();
                    } else if (window.getSelection().removeAllRanges) {
                        window.getSelection().removeAllRanges();
                    }
                } else if (document.selection) {
                    document.selection.empty();
                }

                for (var i = 0, len = savedSel.length; i < len; ++i) {
                    sel.addRange(savedSel[i]);
                }
            } else if (document.selection && savedSel.select) {
                savedSel.select();
            }
        }
    }



    /**
     *  Clean Word. Source:
     *  http://patisserie.keensoftware.com/en/pages/remove-word-formatting-from-rich-text-editor-with-javascript
     *  http://community.sitepoint.com/t/strip-unwanted-formatting-from-pasted-content/16848/3
     *  http://www.1stclassmedia.co.uk/developers/clean-ms-word-formatting.php
     */
    cleanHTML(input, cleanstyle) {

        let stringStripper = /(\n|\r| class=(")?Mso[a-zA-Z]+(")?)/g;
        let output         = input.replace(stringStripper, ' ');

        let commentSripper = new RegExp('<!--(.*?)-->', 'g');
        output             = output.replace(commentSripper, '');

        let tagStripper;
        if (cleanstyle) {
            tagStripper = new RegExp('<(/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>', 'gi');
        } else {
            tagStripper = new RegExp('<(/)*(meta|link|\\?xml:|st1:|o:|font)(.*?)>', 'gi');
        }
        output = output.replace(tagStripper, '');

        let badTags = ['style', 'script', 'applet', 'embed', 'noframes', 'noscript'];

        for (let i = 0; i < badTags.length; i++) {
            tagStripper = new RegExp('<' + badTags[i] + '.*?' + badTags[i] + '(.*?)>', 'gi');
            output      = output.replace(tagStripper, '');
        }

        let badAttributes;
        if (cleanstyle) {
            badAttributes = ['style', 'start'];
        } else {
            badAttributes = ['start'];
        }
        for (let i = 0; i < badAttributes.length; i++) {
            let attributeStripper = new RegExp(' ' + badAttributes[i] + '="(.*?)"', 'gi');
            output                = output.replace(attributeStripper, '');
        }

        // https://gist.github.com/sbrin/6801034
        //output = output.replace(/<!--[\s\S]+?-->/gi, ''); //done (see above)
        //output = output.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '');
        output = output.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s/>]))[^>]*>/gi, '');
        output = output.replace(/<(\/?)s>/gi, '<$1strike>');
        output = output.replace(/&nbsp;/gi, ' ');
        //output = output.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, function(str, spaces) {
        //    return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : '';
        //});

        //clean copied elm-active background-color (LATER: improve)
        output = output.replace(/background-color: rgba\(200, 200, 201, 0.11\);/gi, '');
        output = output.replace(/background-color: rgba\(200, 200, 201, 0.11\)/gi, '');

        return output;
    }

    /**
     *
     */
    checkEmpty() {

        const dom = new Dom();

        // Get all builder areas
        const builders = document.querySelectorAll(this.builder.opts.container);
        Array.prototype.forEach.call(builders, (builder) => {

            const rows = dom.elementChildren(builder);
            let empty  = true;
            rows.forEach((row) => {

                if (dom.hasClass(row, 'row-add-initial')) return;
                if (dom.hasClass(row, 'dummy-space')) return;

                empty = false;
            });

            if (empty) {
                let emptyinfo = builder.querySelector('.row-add-initial');
                if (!emptyinfo) {
                    builder.innerHTML = `<button type="button" class="row-add-initial">${this.out('Empty')}<br><span>${this.out('+ Click to add content')}</span></div>`;
                    emptyinfo         = builder.querySelector('.row-add-initial');
                }
                emptyinfo.addEventListener('click', () => {

                    this.clearActiveCell();

                    dom.addClass(emptyinfo, 'row-active'); // Needed for addContent(). Directly apply class in Util is fine.

                    const builderStuff = this.builderStuff();
                    let quickadd       = builderStuff.querySelector('.quickadd'); // see quickadd.js. Directly select by class in Util is fine.

                    let tabs           = quickadd.querySelector('.is-pop-tabs');
                    tabs.style.display = 'none';

                    const viewportHeight   = window.innerHeight;
                    let top                = emptyinfo.getBoundingClientRect().top;
                    const left             = emptyinfo.getBoundingClientRect().left + emptyinfo.offsetWidth / 2 - 11;
                    quickadd.style.display = 'flex';
                    const w                = quickadd.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
                    const h                = quickadd.offsetHeight;

                    if (viewportHeight - top > h) {
                        top                 = top + emptyinfo.offsetHeight - 19;
                        quickadd.style.top  = (top + window.pageYOffset) + 27 + 'px';
                        quickadd.style.left = (left - w / 2 + 7) + 'px';
                        dom.removeClass(quickadd, 'arrow-bottom');
                        dom.removeClass(quickadd, 'arrow-right');
                        dom.removeClass(quickadd, 'arrow-left');
                        dom.removeClass(quickadd, 'center');
                        dom.addClass(quickadd, 'arrow-top');
                        dom.addClass(quickadd, 'center');
                    } else {
                        quickadd.style.top  = (top + window.pageYOffset - h - 8) + 'px';
                        quickadd.style.left = (left - w / 2 + 7) + 'px';
                        dom.removeClass(quickadd, 'arrow-top');
                        dom.removeClass(quickadd, 'arrow-right');
                        dom.removeClass(quickadd, 'arrow-left');
                        dom.removeClass(quickadd, 'center');
                        dom.addClass(quickadd, 'arrow-bottom');
                        dom.addClass(quickadd, 'center');
                    }

                    quickadd.setAttribute('data-mode', 'row');

                });
            } else {
                let emptyinfo = builder.querySelector('.row-add-initial');
                if (emptyinfo) emptyinfo.parentNode.removeChild(emptyinfo);
            }

        });

    }

    /**
     *
     */
    clearPreferences() {
        localStorage.removeItem('_buildermode'); //builderMode
        localStorage.removeItem('_editingtoolbar'); //toolbar
        localStorage.removeItem('_editingtoolbardisplay'); //toolbarDisplay
        localStorage.removeItem('_hidecelltool'); //columnTool
        localStorage.removeItem('_rowtool'); //rowTool
        localStorage.removeItem('_hideelementtool'); //elementTool
        localStorage.removeItem('_hidesnippetaddtool'); //snippetAddTool
        localStorage.removeItem('_outlinemode'); //outlineMode
        localStorage.removeItem('_hiderowcoloutline'); //rowcolOutline
        localStorage.removeItem('_outlinestyle'); //outlineStyle
        localStorage.removeItem('_hideelementhighlight'); //elementHighlight
        localStorage.removeItem('_opensnippets'); //snippetOpen
        localStorage.removeItem('_toolstyle'); //toolStyle
        localStorage.removeItem('_snippetssidebardisplay'); //snippetsSidebarDisplay

        localStorage.removeItem('_pasteresult'); //DON'T HAVE PROP

        //NOT USED
        localStorage.removeItem('_scrollableeditor');
        localStorage.removeItem('_animatedsorting');
        localStorage.removeItem('_addbuttonplace');
        localStorage.removeItem('_hiderowtool');
        localStorage.removeItem('_dragwithouthandle');
        localStorage.removeItem('_advancedhtmleditor');
        localStorage.removeItem('_hidecolhtmleditor');
        localStorage.removeItem('_hiderowhtmleditor');
    }

    /**
     * source: http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
     */
    pasteHtmlAtCaret(html, selectPastedContent) {

        this.restoreSelection();

        var sel, range;

        if (window.getSelection) {

            if (!this.builder.activeCol) return;

            sel = window.getSelection();
            if (sel.getRangeAt && sel.rangeCount) {

                range = sel.getRangeAt(0);
                range.deleteContents();

                var el       = document.createElement('div');
                el.innerHTML = html;
                var frag     = document.createDocumentFragment(), node, lastNode;
                while ((node = el.firstChild)) {
                    lastNode = frag.appendChild(node);
                }
                var firstNode = frag.firstChild;
                range.insertNode(frag);

                if (lastNode) {
                    range = range.cloneRange();
                    range.setStartAfter(lastNode);
                    if (selectPastedContent) {
                        range.setStartBefore(firstNode);
                    } else {
                        range.collapse(true);
                    }
                    sel.removeAllRanges();
                    if (!this.builder.isTouchSupport) sel.addRange(range);
                }
            }
        } else if ((sel = document.selection) && sel.type !== 'Control') {

            if (!this.builder.activeCol) return;

            var originalRange = sel.createRange();
            originalRange.collapse(true);
            sel.createRange().pasteHTML(html);
            if (selectPastedContent) {
                range = sel.createRange();
                range.setEndPoint('StartToStart', originalRange);
                if (!this.builder.isTouchSupport) range.select();
            }
        }
    }

    /**
     *
     */
    refreshModule() {
        let module = this.builder.activeModule;
        if (!module) return;

        let index     = 1;
        let subblocks = module.querySelectorAll('[data-subblock]');
        Array.prototype.forEach.call(subblocks, (subblock) => {

            let builderhtml = subblock.innerHTML;

            module.setAttribute('data-html-' + index, encodeURIComponent(builderhtml));
            index++;
        });

        let html = decodeURIComponent(module.getAttribute('data-html'));
        html     = html.replace(/{id}/g, this.makeId());

        module.innerHTML = '';

        var range = document.createRange();
        range.setStart(module, 0);
        module.appendChild(
            range.createContextualFragment(html)
        );

        subblocks = module.querySelectorAll('[data-subblock]');
        var i     = 1;
        Array.prototype.forEach.call(subblocks, (subblock) => {
            if (module.getAttribute('data-html-' + i)) {
                subblock.innerHTML = decodeURIComponent(module.getAttribute('data-html-' + i));
            }
            i++;
        });

    }

    /**
     *
     */
    isTouchSupport() {
        if (('ontouchstart' in window) || (navigator.MaxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * https://stackoverflow.com/questions/31757852/how-can-i-detect-internet-explorer-ie-and-microsoft-edge-using-javascript
     */
    detectIE() {
        var ua   = window.navigator.userAgent;
        var msie = ua.indexOf('MSIE ');
        if (msie > 0) {
            return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
        }

        var trident = ua.indexOf('Trident/');
        if (trident > 0) {
            var rv = ua.indexOf('rv:');
            return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
        }

        var edge = ua.indexOf('Edge/');
        if (edge > 0) {
            return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
        }

        return false;
    }

    /**
     * Source: https://css-tricks.com/snippets/javascript/lighten-darken-color/
     */
    LightenDarkenColor(col, amt) {

        var usePound = false;

        if (col[0] === '#') {
            col      = col.slice(1);
            usePound = true;
        }

        var num = parseInt(col, 16);

        var r = (num >> 16) + amt;

        if (r > 255) r = 255;
        else if (r < 0) r = 0;

        var b = ((num >> 8) & 0x00FF) + amt;

        if (b > 255) b = 255;
        else if (b < 0) b = 0;

        var g = (num & 0x0000FF) + amt;

        if (g > 255) g = 255;
        else if (g < 0) g = 0;

        //return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16);
        return (usePound ? '#' : '') + String('000000' + (g | (b << 8) | (r << 16)).toString(16)).slice(-6);
    }
}

/**
 * Dom Class
 */
export class Dom {

    /**
     *
     */
    createElement(tag) {
        return document.createElement(tag);
    }

    /**
     *
     */
    appendChild(parent, child) {
        parent.appendChild(child);
    }

    /**
     *
     */
    appendHtml(parent, html) {
        parent.insertAdjacentHTML('beforeend', html);
    }

    /**
     *
     */
    addEventListener(parent, type, listener) {
        //LATER: if(parent)
        parent.addEventListener(type, listener);
    }

    /**
     *
     */
    addClass(element, classname) {
        if (!element) return;
        if (this.hasClass(element, classname)) return;
        if (element.classList.length === 0) element.className = classname;
        else element.className = element.className + ' ' + classname;
        element.className = element.className.replace(/  +/g, ' ');
        //else element.classList.add(classname); //error if there is -
    }

    /**
     *
     */
    removeClass(element, classname) {
        if (!element) return;
        if (element.classList.length > 0) {
            // element.className = element.className.replace(new RegExp('\\b'+ classname+'\\b', 'g'), '');
            // element.className = element.className.replace(/  +/g, ' ');

            let i, j, imax, jmax;
            let classesToDel = classname.split(' ');
            for (i = 0, imax = classesToDel.length; i < imax; ++i) {
                if (!classesToDel[i]) continue;
                let classtoDel = classesToDel[i];

                // https://jsperf.com/removeclass-methods
                let sClassName     = '';
                let currentClasses = element.className.split(' ');
                for (j = 0, jmax = currentClasses.length; j < jmax; ++j) {
                    if (!currentClasses[j]) continue;
                    if (currentClasses[j] !== classtoDel) sClassName += currentClasses[j] + ' ';
                }
                element.className = sClassName.trim();
            }

            if (element.className === '') element.removeAttribute('class');

        }
    }

    /**
     * https://plainjs.com/javascript/attributes/adding-removing-and-testing-for-classes-9/
     */
    hasClass(element, classname) {
        if (!element) return false;
        try {
            let s = element.getAttribute('class');
            return new RegExp('\\b' + classname + '\\b').test(s);
        } catch (e) {
            // Do Nothing
            // console.log(element);
        }
        //return element.classList ? element.classList.contains(classname) : new RegExp('\\b'+ classname+'\\b').test(element.className);
    }

    /**
     *
     */
    moveAfter(element, targetElement) {
        targetElement.parentNode.insertBefore(element, targetElement);
        targetElement.parentNode.insertBefore(targetElement, targetElement.previousElementSibling);
    }

    /**
     * https://stackoverflow.com/questions/10381296/best-way-to-get-child-nodes
     */
    elementChildren(element) {
        const childNodes = element.childNodes;
        let children     = [];
        let i            = childNodes.length;
        while (i--) {
            if (childNodes[i].nodeType === 1 /*&& childNodes[i].tagName === 'DIV'*/) {
                children.unshift(childNodes[i]);
            }
        }
        return children;
    }

    /**
     *
     */
    parentsHasClass(element, classname) {
        while (element) {
            // if(classname==='is-side') console.log(element.nodeName); // NOTE: click on svg can still returns undefined in IE11
            if (!element.tagName) return false;
            if (element.tagName === 'BODY' || element.tagName === 'HTML') return false;
            // if(!element.classList) {
            //     console.log('no classList');
            //     return false;
            // }
            if (this.hasClass(element, classname)) {
                return true;
            }
            // TODO: if(element.nodeName.toLowerCase() === 'svg') console.log(element);
            element = element.parentNode;
        }
    }

    /**
     *
     */
    parentsHasId(element, id) {
        while (element) {
            if (!element.tagName) return false;
            if (element.tagName === 'BODY' || element.tagName === 'HTML') return false;
            if (element.id === id) {
                return true;
            }
            element = element.parentNode;
        }
    }

    /**
     *
     */
    parentsHasTag(element, tagname) {
        while (element) {
            if (!element.tagName) return false;
            if (element.tagName === 'BODY' || element.tagName === 'HTML') return false;
            if (element.tagName.toLowerCase() === tagname.toLowerCase()) {
                return true;
            }
            element = element.parentNode;
        }
    }

    /**
     *
     */
    parentsHasAttribute(element, attrname) {
        while (element) {
            if (!element.tagName) return false;
            if (element.tagName === 'BODY' || element.tagName === 'HTML') return false;
            try {
                if (element.hasAttribute(attrname)) { // error on svg element
                    return true;
                }
            } catch (e) {
                // Do Nothing
                // console.log(element);
                // return false;
            }
            element = element.parentNode;
        }
    }

    /**
     *
     */
    parentsHasElement(element, tagname) {
        while (element) {
            if (!element.tagName) return false;
            if (element.tagName === 'BODY' || element.tagName === 'HTML') return false;

            element = element.parentNode;

            if (!element) return false;
            if (!element.tagName) return false;

            if (element.tagName.toLowerCase() === tagname) {
                return true;
            }
        }
    }

    /**
     *
     */
    removeClasses(elms, classname) {
        for (let i = 0; i < elms.length; i++) {
            elms[i].classList.remove(classname);
        }
    }

    /**
     *
     */
    removeAttributes(elms, attrname) {
        for (let i = 0; i < elms.length; i++) {
            elms[i].removeAttribute(attrname);
        }
    }

    /**
     *
     */
    removeElements(elms) {
        Array.prototype.forEach.call(elms, (el) => {
            el.parentNode.removeChild(el);
        });
    }

    /**
     * source: https://stackoverflow.com/questions/2871081/jquery-setting-cursor-position-in-contenteditable-div
     */
    moveCursorToElement(element) {
        var sel, range;
        if (window.getSelection && document.createRange) {
            range = document.createRange();
            range.selectNodeContents(element);
            range.collapse(false);
            sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
        } else if (document.body.createTextRange) {
            range = document.body.createTextRange();
            range.moveToElementText(element);
            range.collapse(false);
            range.select();
        }
    }

    /**
     * source: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element
     */
    selectElementContents(el) {
        var range = document.createRange();
        range.selectNodeContents(el);
        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * Get selected text
     */
    getSelected() {
        if (window.getSelection) {
            return window.getSelection().toString();
        } else if (document.getSelection) {
            return document.getSelection().toString();
        } else {
            var selection = document.selection && document.selection.createRange();
            if (selection.text) {
                return selection.text;
            }
            return false;
        }
    }

    /**
     *
     */
    checkEditable() {
        try {
            var el;
            var curr;
            if (window.getSelection) {
                curr = window.getSelection().getRangeAt(0).commonAncestorContainer;
                if (curr.nodeType === 3) {  //ini text node
                    el = curr.parentNode;
                } else {
                    el = curr;
                }
            } else if (document.selection) {
                curr = document.selection.createRange();
                el   = document.selection.createRange().parentElement();
            }
            if (this.parentsHasAttribute(el, 'contenteditable')) return true;
            else return false;
        } catch (e) {
            return false;
        }
    }

    /**
     *
     */
    textSelection() {
        try {
            var elm;
            var curr = window.getSelection().getRangeAt(0).commonAncestorContainer;
            //console.log(curr.nodeType)
            if (curr.nodeType === 3) {  // text node
                elm = curr.parentNode;
                if (this.parentsHasClass(elm, 'is-builder')) {
                    return elm;
                } else {
                    return false;
                }
            } else {
                elm          = curr;
                var nodeName = elm.nodeName.toLowerCase();
                if (nodeName === 'i' && elm.innerHTML === '') { //icon
                    if (this.parentsHasClass(elm, 'is-builder')) {
                        return elm;
                    }
                }

                // Check if a block (because when placing cursor using arrow keys on empty block, nodeType=1 not 3)
                if (nodeName === 'p' || nodeName === 'h1' || nodeName === 'h2'
                    || nodeName === 'h3' || nodeName === 'h4' || nodeName === 'h5'
                    || nodeName === 'h6' || nodeName === 'li' || nodeName === 'pre'
                    || nodeName === 'blockquote') {
                    return elm;
                }
                return false;
            }
        } catch (e) {
            return false;
        }
    }

    /**
     *
     */
    getStyle(element, property) {
        return window.getComputedStyle ? window.getComputedStyle(element, null).getPropertyValue(property) : element.style[property.replace(/-([a-z])/g, function (g) {
            return g[1].toUpperCase();
        })];
    }

}

