All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.dist.module-debug.mikado.js Maven / Gradle / Ivy

The newest version!
/**!
 * Mikado.js
 * Copyright 2019-2024 Nextapps GmbH
 * Author: Thomas Wilkerling
 * Licence: Apache-2.0
 * https://github.com/nextapps-de/mikado
 */

import { TemplateDOM, Template, MikadoOptions, MikadoCallbacks, NodeCache, ProxyCache } from "./type.js";
import Observer from "./array.js";
import { create_path, construct } from "./factory.js";
import proxy_create from "./proxy.js";

/** @const {Object>} */
export const includes = Object.create(null);

/**
 * Abbrevations:
 * _mkd: _dom
 * _mkl: _live
 * _mki: _instance
 */

/**
 * NOTE: Using prototype enables conditional instance member functions.
 * @param {!string|Template} template
 * @param {MikadoOptions=} options
 * @constructor
 */

export default function Mikado(template, options = /** @type MikadoOptions */{}) {

    if (!(this instanceof Mikado)) {

        return new Mikado(template, options);
    }

    if ("string" == typeof template) {

        const tpl = includes[template];

        if (!tpl) {

            throw new Error("The template '" + template + "' is not registered.");
        }


        if (tpl instanceof Mikado) {

            return tpl;
        }

        template = /** @type Template */tpl[0];
        options || (options = /** @type MikadoOptions */tpl[1]);
    }

    if (!template) {

        throw new Error("Initialization Error: Template is not defined.");
    }

    if (!template.tpl /*|| !template.name*/) {

            throw new Error("Initialization Error: Template isn't supported.");
        }

    /** @type {Array} */
    this.dom = [];
    /** @type {number} */
    this.length = 0;
    /** @type {?Element} */
    this.root = options.root || options.mount || null;
    /** @const {boolean} */
    this.recycle = !!options.recycle;
    /** @type {*} */
    this.state = options.state || {};
    /** @type {boolean} */
    this.shadow = options.shadow || !!template.cmp;

    /** @const {string} */
    this.key = template.key || "";
    /**
     * @private
     * @dict {Object}
     */
    this.live = {};

    /** @type {Array} */
    const fn = template.fn;
    // make a copy to make this template re-usable when consumed
    // this should just have been copied when it is a root template!
    template.fc || fn && (template.fc = fn.slice());
    /**
     * The compiler unshift includes, so we can use faster arr.pop() here, because it needs reverse direction.
     * Let consume the nested template functions by using .pop() is pretty much simpler than using an index.
     * @private {Function}
     */
    this.apply = fn && fn.pop();
    /** @type {Template|null} */
    this.tpl = template;
    /** @const {string} */
    this.name = template.name;

    /*
     * Includes are filled with Mikado instances when factory is constructed
     * @const {Array}
     */
    this.inc = [];

    const cacheable = this.recycle || !!this.key;

    // Pool sizes are automatically being handled,
    // non-keyed pools does not need boundary,
    // keyed pools start at size 1 and increase to max items per view

    /** @type {number} */
    this.pool = cacheable && options.pool ? 1 : 0;
    /** @private {Array} */
    this.pool_shared = [];

    /** @private */
    this.pool_keyed = new Map();


    /** @const {boolean} */
    this.cache = cacheable && (template.cache || !!options.cache);


    /** @type {boolean} */
    this.async = !!options.async;
    /** @private {number} */
    this.timer = 0;


    /** @private {MikadoCallbacks|null} */
    this.on = options.on || null;
    {

        /**
         * @type {Object>}
         */
        this.proxy = null;
        /** @type {number} */
        this.fullproxy = 0;
        /** @type {Observer|undefined} */
        const store = options.observe;

        if (store) {

            new Observer(store).mount(this);
        }
    }

    if (this.root) {

        this.mount(this.root, options.hydrate);
    } else {

        /** @private */
        this.factory = null;
    }
}

/**
 * This function is also automatically called when loading es5 templates.
 * @param {string|Template} tpl
 * @param {MikadoOptions=} options
 */

export function register(tpl, options) {

    let name, re_assign;

    if ("string" == typeof tpl) {

        name = re_assign = tpl;
        tpl = /** @type {string|Template} */includes[name];
        tpl instanceof Mikado || (tpl = tpl[0]);

        if (!tpl) {

            throw new Error("The template '" + name + "' was not found.");
        }
    } else {

        name = tpl.name;
    }

    if (includes[name]) {

        if (re_assign) {

            console.info("The template '" + name + "' was replaced by a new definition.");
        } else {

            console.warn("The template '" + name + "' was already registered and is getting overwritten. When this isn't your intention, then please check your template names about uniqueness and collision!");
        }
    }

    // Just collect template definitions. The instantiation starts when .mount() is
    // internally called for the first time.

    includes[name] = [
    /** @type {Template} */tpl,
    /** @type {MikadoOptions} */options];

    return Mikado;
}

/**
 * @param {string|Template} name
 */

export function unregister(name) {

    if ("object" == typeof name) {

        name = /** @type {string} */name.name;
    }

    const mikado = includes[name];

    if (mikado) {

        mikado instanceof Mikado && mikado.destroy();
        includes[name] = null;
    }

    return Mikado;
}

/*

    Example: Swap Mount

    A[DOM] B[DOM]
    A[TPL] A[TPL]

    A[DOM] A[DOM]
    A[TPL] B[TPL]

 */

/**
 * @param {Element} target
 * @param {boolean=} hydrate
 * @returns {Mikado}
 * @const
 */

Mikado.prototype.mount = function (target, hydrate) {

    if (!target) {

        throw new Error("No target was passed to .mount()");
    }


    // cancel async render task if scheduled
    this.timer && this.cancel();


    if (this.shadow) {

        // Actually the MIKADO_CLASS will append to the templates root element,
        // but it would be better to have it on the mounting element
        // TODO improve getting the root withing components by assigning MIKADO_ROOT to the mounting element

        const cmp = /** @type {Array} */this.tpl.cmp;
        // also when cmp: [] has no definitions at top level scope
        target = target.shadowRoot || target.attachShadow({ mode: "open" });

        if (cmp && cmp.length) {

            // the root is always the last element
            const tmp = target.lastElementChild;

            if (tmp /*&& tmp.tagName === "ROOT"*/) {

                    target = tmp;
                } else {
                // push root as the last element
                cmp.push({ tag: "root" });

                /** @type {TemplateDOM} */


                for (let i = 0, node; i < cmp.length; i++) {

                    node = construct(this, /** @type {TemplateDOM} */cmp[i], [], "");
                    target.append(node);

                    if (i === cmp.length - 1) {

                        // the root element is the last one
                        target = /** @type {Element} */node;
                    }
                }
            }
        }
    }

    const target_instance = target._mki,
          root_changed = this.root !== target;


    if (target_instance === this) {

        // same template, same root

        if (!root_changed) return this;

        // same template, different root

        this.dom = target._mkd;
        this.length = this.dom.length;
    } else if (target_instance) {

        target_instance.clear();

        // different template

        target._mkd = this.dom = [];
        this.length = 0;

        if (target.firstChild) {
            target.textContent = "";
        }

        const callback = this.on && this.on.unmount;
        callback && callback(target, target_instance);
    } else {

        // initial mount

        if (hydrate) {

            this.dom = collection_to_array(target.children);
            this.length = this.dom.length;
        } else {

            this.dom = [];
            this.length = 0;

            if (target.firstChild) {
                target.textContent = "";
            }
        }

        target._mkd = this.dom;
    }

    if (this.key) {

        // handle live pool

        if (root_changed && this.root) {

            this.root._mkl = this.live;
        }

        if (target_instance === this) {

            this.live = target._mkl;
        } else {

            const live = {};

            if (!target_instance && hydrate && this.length) {

                for (let i = 0, node, key; i < this.length; i++) {

                    node = this.dom[i];
                    // server-side rendering needs to export the key as attribute
                    key = node.getAttribute("key");

                    if (!key) {

                        console.warn("The template '" + this.name + "' runs in keyed mode, but the hydrated component don't have the attribute 'key' exported.");
                    }


                    node._mkk = key;
                    live[key] = node;
                }
            }

            target._mkl = this.live = live;
        }
    }

    target._mki = this;
    this.root = target;

    if (!this.factory) {

        if (hydrate && this.length) {

            /** @private */
            this.factory = this.dom[0].cloneNode(!0);
            construct(this, /** @type {TemplateDOM} */this.tpl.tpl, [], "", this.factory) && finishFactory(this);
        }

        // also when falls back on hydration if structure was incompatible:

        if (this.tpl) {

            /** @private */
            this.factory = construct(this, /** @type {TemplateDOM} */this.tpl.tpl, [], "");
            finishFactory(this);
        }
    }

    const callback = this.on && this.on.mount;
    callback && callback(target, this);

    return this;
};

/**
 * @param {Mikado} self
 */

function finishFactory(self) {

    if (self.tpl.fc) {

        // self.tpl.fn could have further template functions

        self.tpl.fn = self.tpl.fc;
        self.tpl.fc = null;
    }

    self.tpl = null;
}

/**
 * @param {NodeList} collection
 * @return {Array}
 */

function collection_to_array(collection) {
    const length = collection.length,
          array = Array(length);


    for (let i = 0; i < length; i++) {

        array[i] = collection[i];
    }

    return array;
}

/**
 * @param {Element|ShadowRoot} root
 * @param {Template} template
 * @param {Array|Object|Function|boolean=} data
 * @param {*|Function|boolean=} state
 * @param {Function|boolean=} callback
 * @const
 */

export function once(root, template, data, state, callback) {

    if (!root) {

        throw new Error("Root element is not defined.");
    }

    if (!template) {

        throw new Error("Template is not defined.");
    }


    let mikado;

    if ("function" == typeof data || !0 === data) {

        callback = data;
        data = null;
    } else if ("function" == typeof state || !0 === state) {

        callback = state;
        state = null;
    }

    if (callback) {

        return new Promise(function (resolve) {

            requestAnimationFrame(function () {

                once(root, template, data, state);
                if ("function" == typeof callback) callback();
                resolve();
            });
        });
    }
    const is_shadow = template.cmp,
          is_component = is_shadow && is_shadow.length;


    if (is_shadow && !is_component) {

        // switch to shadow root

        root = root.shadowRoot || root.attachShadow({ mode: "open" });
    }

    if (!data && !is_component && !template.fn) {

        // static non-looped templates
        // uses the low-level template factory constructor

        const node = construct(
        /** @type {!Mikado} */{},
        /** @type {TemplateDOM} */template.tpl, [], "", null, 1);

        root.append(node);
    } else {

        mikado = new Mikado(template);

        if (is_component) {

            // full declared web components needs to be mounted

            root = mikado.mount(root).root;
        }

        if (data && Array.isArray(data)) {

            // looped templates

            for (let i = 0; i < data.length; i++) {

                root.append(mikado.create(data[i], state, i));
            }
        } else {

            // dynamic non-looped templates + web components

            root.append(mikado.create(data, state));
        }

        mikado.destroy();
    }

    return Mikado;
}

/**
 * @param {!*} data
 * @param {*=} state
 * @param {Function|boolean=} callback
 * @param {boolean|number=} _skip_async
 * @returns {Mikado|Promise}
 * @const
 */

Mikado.prototype.render = function (data, state, callback, _skip_async) {

    if (!this.root) {

        throw new Error("Template was not mounted or root was not found.");
    } else if (this.root._mki !== this) {

        throw new Error("Another template is already assigned to this root. Please use '.mount(root_element)' before calling '.render()' to switch the context of a template.");
    }


    if (!_skip_async) {

        let has_fn;

        if (state && (has_fn = "function" == typeof state) || !0 === state) {

            callback = /** @type {Function|boolean} */state;
            state = null;
        }

        if (this.timer) {

            this.cancel();
        }

        if (this.async || callback) {

            const self = this;
            has_fn || (has_fn = "function" == typeof callback);

            self.timer = requestAnimationFrame(function () {

                self.timer = 0;
                self.render(data, state, null, 1);
                /*has_fn &&*/ /** @type {Function} */callback();
            });

            return has_fn ? this : new Promise(function (resolve) {

                callback = resolve;
            });
        }
    }

    //profiler_start("render");

    let length = this.length;

    // a template could have just expressions without accessing data

    if (!data) {

        if (!this.apply) {

            this.dom[0] || this.add();
            return this;
        }
    }

    let count;

    if (Array.isArray(data) || data instanceof Observer) {

        count = data.length;

        if (!count) {

            return this.remove(0, length);
        }
    } else {

        if (this.proxy) {

            throw new Error("When a template is using data bindings by an expression like {{= ... }} you will need to pass an array to the render() function, also when just one single item should be rendered. Because the array you will pass in is getting proxified after calling .render(arr), after then you can trigger bindings via arr[0].prop = 'value'.");
        }

        data = [data];
        count = 1;
    }

    const key = this.key,
          proxy = this.proxy;


    if (length && !key && !this.recycle) {

        this.remove(0, length);
        length = 0;
    }

    let min = length < count ? length : count,
        x = 0;


    // update
    if (x < min) {

        for (let node, item; x < min; x++) {

            node = this.dom[x];
            item = data[x];

            if (key && node._mkk !== item[key]) {

                return this.reconcile( /** @type {Array} */data, state, x);
            } else {

                this.update(node, item, state, x);
            }

            if (proxy && /* (!key && !this.recycle) || */!item._mkx) {

                data[x] = apply_proxy(this, node, item);
            }
        }
    }

    // add
    if (x < count) {

        // when recycle is disabled the proxy needs to be updated
        const recycle = key || this.recycle;

        for (; x < count; x++) {

            const item = data[x];
            this.add(item, state /*, x*/);

            if (proxy && (!recycle || !item._mkx)) {

                data[x] = apply_proxy(this, this.dom[x], item);
            }
        }
    }

    // remove
    else if (count < length) {

            this.remove(count, length - count);
        }

    //profiler_end("render");

    return this;
};

/**
 * @param {!Element|number} node
 * @param {*=} data
 * @param {*=} state
 * @param {number=} index
 * @const
 */

Mikado.prototype.replace = function (node, data, state, index) {

    //profiler_start("replace");

    if ("undefined" == typeof index) {

        if ("number" == typeof node) {

            index = 0 > node ? this.length + node : node;
            node = this.dom[index];
        } else {

            index = this.index(node);
        }
    }

    node = /** @type {!Element} */node;

    let tmp, update;

    // The main difference of replace() and update() is that replace() will also handle the keyed live pool.

    if (this.key) {

        const key = data[this.key];

        if (tmp = this.live[key]) {

            if (tmp !== node) {
                const index_old = this.index(tmp),
                      node_a = index_old < index ? tmp : node,
                      node_b = index_old < index ? node : tmp;

                let next = this.dom[index_old < index ? index_old + 1 : index + 1];

                this.dom[index] = tmp;
                this.dom[index_old] = node;

                // if(next === node_b){
                //
                //     this.root.insertBefore(node_b, node_a);
                // }
                // else{

                if (next !== node_b) {

                    this.root.insertBefore(node_a, node_b);
                } else {

                    next = node_a;
                }

                this.root.insertBefore(node_b, next);
                //}
            }
        } else if (this.pool && (tmp = this.pool_keyed.get(key))) {

            this.pool_keyed.delete(key);
            this.checkout(node);
            this.dom[index] = tmp;
            node.replaceWith(tmp);
        }

        update = tmp;
    } else if (this.recycle) {

        update = node;
    }

    if (update) {

        this.apply && (this.fullproxy && data._mkx || this.apply(data, state || this.state, index, update._mkp || create_path(update, this.factory._mkp, this.cache)));
    } else {

        const clone = this.create(data, state, index, 1);

        if (this.key || this.pool) {

            this.checkout(node);
        }

        this.dom[index] = clone;
        node.replaceWith(clone);
    }

    const callback = this.on && this.on.replace;
    callback && callback(node, this);

    //profiler_end("replace");

    return this;
};

/**
 * @param {Element|number} node
 * @param {*=} data
 * @param {*=} state
 * @param {number=} index
 * @const
 */

Mikado.prototype.update = function (node, data, state, index) {

    //profiler_start("update");

    if (!this.apply) {

        if ("number" != typeof index) {

            console.warn("The template '" + this.name + "' is a static template and should not be updated. Alternatively you can use .replace() to switch contents.");
        }

        return this;
    }

    if (this.fullproxy && data._mkx) {

        return this;
    }

    if ("undefined" == typeof index) {

        if ("number" == typeof node) {

            index = 0 > node ? this.length + node - 1 : node;
            node = this.dom[index];
        } else {

            index = this.index(node);
        }
    }

    node = /** @type {Element} */node;

    // Is keyed handling also needed in update?
    // .replace() = .update() + keyed handling

    /*
    if(!_skip_check){
          let replace;
          if(SUPPORT_KEYED && this.key){
              const ref = node[MIKADO_TPL_KEY];
            const tmp = data[this.key];
              if(ref !== tmp){
                  if(this.recycle){
                      this.live[ref] = null;
                    this.live[tmp] = node;
                    node[MIKADO_TPL_KEY] = tmp;
                }
                else{
                      replace = true;
                }
            }
        }
        else if(!this.recycle){
              replace = true;
        }
          if(replace){
              return this.replace(node, data, state, index);
        }
    }
    */

    this.apply(data, state || this.state, index, node._mkp || create_path(node, this.factory._mkp, this.cache));

    //profiler_end("update");

    const callback = this.on && this.on.update;
    callback && callback(node, this);

    return this;
};

/** @const */
Mikado.prototype.cancel = function () {

    //if(this.timer){

    cancelAnimationFrame(this.timer);
    this.timer = 0;
    //}

    return this;
};

/**
 * @param {*=} data
 * @param {*=} state
 * @param {number=} index
 * @param {boolean|number=} _update_pool
 * @return {!Element}
 * @const
 */

Mikado.prototype.create = function (data, state, index, _update_pool) {
    const keyed = this.key,
          key = keyed && data[keyed];

    let node, pool, factory, found;

    if (this.pool) {

        // 1. shared keyed pool
        if (keyed) {

            if ((pool = this.pool_keyed) && (node = pool.get(key))) {
                pool.delete(key);
                found = 1;
            }
        }
        // 2. shared non-keyed pool
        else if ((pool = this.pool_shared) && pool.length) {
                node = pool.pop();
            }
    }

    if (!node) {

        node = factory = this.factory;

        if (!factory) {

            /** @private */
            this.factory = node = factory = construct(this, /** @type {TemplateDOM} */this.tpl.tpl, [], "");
            finishFactory(this);
        }
    }

    let cache;

    if (this.apply) {

        const vpath = node._mkp || create_path(node, this.factory._mkp, !!factory || this.cache);
        cache = factory && this.cache && /** @type {Array} */Array(vpath.length);

        this.apply(data, state || this.state, index, vpath, !!factory, cache);
    }

    if (factory) {
        node = factory.cloneNode(!0);

        if (cache && !0 !== cache) {

            node._mkc = cache;
        }

        node._mkr = 1;
    }

    if (keyed) {

        if (!found) node._mkk = key;
        if (_update_pool) this.live[key] = node;
    }

    const callback = this.on && this.on[factory ? "create" : "recycle"];
    callback && callback(node, this);

    return node;
};

/**
 * @param {*=} data
 * @param {*|number=} state
 * @param {number|null=} index
 * @returns {Mikado}
 * @const
 */

Mikado.prototype.add = function (data, state, index) {
    //profiler_start("add");

    let has_index;

    if ("number" == typeof state) {

        index = 0 > state ? this.length + state : state;
        state = null;
        has_index = index < this.length;
    } else if ("number" == typeof index) {

        if (0 > index) index += this.length;
        has_index = index < this.length;
    } else {

        index = this.length;
    }

    const node = this.create(data, state, index, 1);

    if (has_index) {

        this.root.insertBefore(node, this.dom[index]);
        splice(this.dom, this.length - 1, index, node);
        //this.dom.splice(length, 0, node);
        this.length++;
    } else {

        this.root.appendChild(node);
        this.dom[this.length++] = node;
    }

    if (this.key && !0 && this.pool) {

        // adjust keyed pool size
        if (this.pool < this.length) this.pool = this.length;
    }

    const callback = this.on && this.on.insert;
    callback && callback(node, this);

    //profiler_end("add");

    return this;
};

/**
 * @param {Mikado} self
 * @param {Element} node
 * @param {Object} data
 * @return {Proxy}
 */

export function apply_proxy(self, node, data) {

    //TODO inject the full path on first recycle

    return proxy_create(data, node._mkp || create_path(node, self.factory._mkp, self.cache), self.proxy);
}

// Since there don't exist a native transaction feature for DOM changes, all changes applies incrementally.
// For a full render task there are a "dirty" intermediate state when moving one node.
// This state will resolve after running through the whole reconcile().
// Since we don't use an extra loop running upfront to calculate the diff,
// Mikado uses a smart algorithm which can find the shortest path in one loop.
// That also means, during reconcile there is no look-ahead to the data (just to the live pool).
// For this reason the keyed live pool needs to be in sync with the vdom array.
// The reconcile runs like a resizable "window function" on where unmatched things
// will move further to the end until the process cursor reach this index.

/**
 * Reconcile based on "longest distance" strategy by Thomas Wilkerling
 * @param {Array=} b
 * @param {*=} state
 * @param {number=} x
 * @returns {Mikado}
 * @const
 */

Mikado.prototype.reconcile = function (b, state, x) {
    const a = this.dom,
          live = this.live,
          key = this.key;
    let end_b = b.length,
        end_a = a.length,
        max_end = end_a > end_b ? end_a : end_b,
        shift = 0;


    for (x || (x = 0); x < max_end; x++) {

        let found;

        if (x < end_b) {
            const b_x = b[x],
                  ended = x >= end_a;
            let a_x, b_x_key, a_x_key, proxy;


            if (this.proxy) {

                if (b_x._mkx) {

                    proxy = this.fullproxy;
                } else {

                    b[x] = apply_proxy(this, a[x], b_x);
                }
            }

            if (!ended) {

                a_x = a[x];
                b_x_key = b_x[key];
                a_x_key = a_x._mkk;

                if (a_x_key === b_x_key) {

                    proxy || this.update(a_x, b_x, state, x);
                    continue;
                }
            }

            if (ended || !live[b_x_key]) {

                // without pool enabled .add() is better than .replace()

                if (ended || !this.pool) {

                    end_a++;
                    max_end = end_a > end_b ? end_a : end_b;

                    this.add(b_x, state, x);
                } else {

                    // TODO replace iteratively performs pool size adjustment
                    this.replace( /** @type {!Element} */a_x, b_x, state, x);
                }

                continue;
            }

            let idx_a, idx_b;

            for (let y = x + 1; y < max_end; y++) {

                // determine longest distance
                if (!idx_a && y < end_a && a[y]._mkk === b_x_key) idx_a = y + 1;
                if (!idx_b && y < end_b && b[y][key] === a_x_key) idx_b = y + 1;

                if (idx_a && idx_b) {

                    // shift up (move target => current)
                    if (idx_a >= idx_b + shift) {

                        const tmp_a = a[idx_a - 1];

                        // when distance is 1 it will always move before, no predecessor check necessary
                        this.root.insertBefore( /** @type {Node} */tmp_a, /** @type {Node} */a_x);

                        proxy || this.update(tmp_a, b_x, state, x);

                        // fast path optimization when distance is equal (skips finding on next turn)
                        if (idx_a === idx_b) {

                            if (1 < y - x) {

                                this.root.insertBefore( /** @type {Node} */a_x, /** @type {Node} */a[idx_a]);
                            }

                            a[x] = a[y];
                            a[y] = /** @type {!Element} */a_x;

                            // internal state validation
                            if (!a_x) console.error("reconcile.error 1");
                        } else {

                            // internal cursor validation
                            if (idx_a - 1 === x) console.error("reconcile.error 2");


                            splice(a, idx_a - 1, x);
                            //a.splice(x, 0, a.splice(idx_a - 1, 1)[0]);

                            shift++;
                        }
                    }
                    // shift down (move current => target)
                    else {

                            const index = idx_b - 1 + shift;

                            // distance is always greater than 1, no predecessor check necessary
                            this.root.insertBefore( /** @type {Node} */a_x, /** @type {Node} */a[index] || null);

                            if ((index > end_a ? end_a : index) - 1 === x) console.error("reconcile.error 3");


                            splice(a, x, (index > end_a ? end_a : index) - 1);
                            //a.splice(/* one is removed: */ index - 1, 0, a.splice(x, 1)[0]);

                            shift--;
                            x--;
                        }

                    found = 1;
                    break;
                }
            }
        }

        if (!found) {

            this.remove(x);

            end_a--;
            max_end = end_a > end_b ? end_a : end_b;
            x--;
        }
    }

    return this;
};

/**
 * @param {Array} arr
 * @param {number} pos_old
 * @param {number} pos_new
 * @param {*=} insert
 * @const
 */

function splice(arr, pos_old, pos_new, insert) {

    const tmp = insert || arr[pos_old];

    if (insert) {

        pos_old++;
    }

    if (pos_old < pos_new) {

        for (; pos_old < pos_new; pos_old++) {

            arr[pos_old] = arr[pos_old + 1];
        }
    } else /*if(pos_old > pos_new)*/{

            for (; pos_old > pos_new; pos_old--) {

                arr[pos_old] = arr[pos_old - 1];
            }
        }

    arr[pos_new] = tmp;
}

/**
 * @param {*=} data
 * @param {*=} state
 * @param {number=} index
 * @const
 */

Mikado.prototype.append = function (data, state, index) {
    //profiler_start("append");

    let has_index;

    if ("number" == typeof state) {

        index = 0 > state ? this.length + state : state;
        state = null;
        has_index = 1;
    } else if ("number" == typeof index) {

        if (0 > index) index += this.length;
        has_index = 1;
    }

    const count = data.length;

    for (let x = 0; x < count; x++) {

        this.add(data[x], state, has_index ? index++ : null);
    }

    //profiler_end("append");

    return this;
};

/**
 * @returns {Mikado}
 * @const
 */

Mikado.prototype.clear = function () {

    if (this.length) {

        this.remove(0, this.length);
    }

    return this;
};

/**
 * @param {!Element|number} index
 * @param {number=} count
 * @returns {Mikado}
 * @const
 */

Mikado.prototype.remove = function (index, count) {

    //profiler_start("remove");

    let length = this.length;

    if (length && index) {

        if ("number" != typeof index) {

            index = this.index(index);
        } else if (0 > index) {

            index = length + index;
        }
    }

    if (!length || index >= length) {

        //profiler_end("remove");

        return this;
    }

    if (count) {

        if (0 > count) {

            index -= count + 1;

            if (0 > index) {

                index = 0;
            }

            count *= -1;
        }
    } else {

        count = 1;
    }

    let nodes;

    if (!index && count >= length) {

        nodes = this.dom;
        count = nodes.length;
        this.root.textContent = "";
        this.root._mkd = this.dom = [];
        length = 0;
    } else {

        nodes = this.dom.splice( /** @type {number} */index, count);
        length -= count;
    }

    // Reverse is applied in order to use push/pop rather than shift/unshift.
    // When no keyed pool is used a proper order of items will:
    // 1. optimize the pagination of content (forward, backward)
    // 2. optimize toggling the count of items per page (list resizing)
    // 3. optimize folding of content (show more, show less)
    // 4. optimize filtering state of content (filter on, filter off)
    // 5. optimize initializing with the last view state (close view, open view)

    const reverse = this.pool && !this.key,
          checkout = this.key || this.pool,
          callback = this.on && this.on.remove;

    let adjust_pool = this.key && this.pool;

    if (adjust_pool && count >= adjust_pool) {

        this.pool_keyed = new Map();
        //this.pool_keyed.clear();
        adjust_pool = 0;
    }

    for (let x = 0, node; x < count; x++) {

        node = nodes[reverse ? count - x - 1 : x];
        length && node.remove();
        checkout && this.checkout(node, /* skip pool resize */1);
        callback && callback(node, this);
    }

    if (adjust_pool && 0 < (adjust_pool = this.pool_keyed.size - adjust_pool)) {

        const keys = this.pool_keyed.keys();

        while (adjust_pool--) {

            this.pool_keyed.delete(keys.next().value);
        }
    }

    this.length = length;

    //profiler_end("remove");

    return this;
};

/**
 * @param {Element} node
 * @return {number}
 * @const
 */

Mikado.prototype.index = function (node) {
    return node ? this.dom.indexOf(node) : -1;
};

/**
 * @param {number} index
 * @return {Element}
 * @const
 */

Mikado.prototype.node = function (index) {
    return this.dom[index];
};

/**
 * @param {Element} node
 * @param {?number=} _skip_resize
 * @private
 * @const
 */

Mikado.prototype.checkout = function (node, _skip_resize) {

    let key;

    if (this.key) {

        key = node._mkk;
        // remove from live-pool
        this.live[key] = null;
    }

    if (this.pool) {

        if (key) {

            // always adding to keyed shared-pool increases the probability of matching keys
            // but requires resizing of limited pools
            this.pool_keyed.set(key, node);

            if (!_skip_resize && /*this.pool !== true &&*/this.pool_keyed.size > this.pool) {

                this.pool_keyed.delete( /*this.pool_keyed._keys ||*/this.pool_keyed.keys().next().value);
            }
        } else {

            const length = this.pool_shared.length;

            //if(this.pool === true || (length < this.pool)){

            // add to non-keyed shared-pool
            this.pool_shared[length] = node;
            //}
        }
    }
};


Mikado.prototype.flush = function () {

    this.pool_shared = [];

    this.pool_keyed = new Map();


    return this;
};

/**
 * @const
 */

Mikado.prototype.destroy = function () {

    for (let i = 0, inc; i < this.inc.length; i++) {

        inc = this.inc[i];
        includes[inc.name] || inc.destroy();
    }

    if (this.key) {

        this.root && (this.root._mkl = null);
        this.live = null;
    }

    if (this.root) {

        this.root._mkd = this.root._mki = null;
    }

    /** @suppress {checkTypes} */this.dom = this.root = this.tpl = this.apply = this.inc = this.state = this.factory = null;

    this.pool_shared = null;

    this.pool_keyed = null;


    this.on = null;


    this.proxy = null;
};