Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
package.dist.module-debug.mikado.js Maven / Gradle / Ivy
Go to download
Web's fastest template library to build user interfaces.
/**!
* 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;
};