674 lines
18 KiB
JavaScript
674 lines
18 KiB
JavaScript
|
// @ts-check
|
||
|
|
||
|
/* RDOM (rotonde dom)
|
||
|
* 0x0ade's collection of DOM manipulation functions because updating innerHTML every time a single thing changes isn't cool.
|
||
|
* This started out as a mini framework for Rotonde.
|
||
|
* Mostly oriented towards quickly manipulating many things quickly.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {{ fields: any[], renderers: any[], texts: any[], html: string, init: null | ((el: null | Element) => (null | Element)) }} RDOMParseResult
|
||
|
*/
|
||
|
|
||
|
export var rdom = {
|
||
|
/** @type {Map<string, Element>} */
|
||
|
_cachedTemplates: new Map(),
|
||
|
/** @type {Map<TemplateStringsArray, number[]>} */
|
||
|
_cachedIDs: new Map(),
|
||
|
_lastID: -1,
|
||
|
|
||
|
/**
|
||
|
* Find an element with a given key.
|
||
|
* @param {Element} el Element (context) to search inside.
|
||
|
* @param {string | number} key Key to look for.
|
||
|
* @returns {HTMLElement} Found element.
|
||
|
*/
|
||
|
find(el, key, value = "", type = "field") {
|
||
|
let attr = `rdom-${type}${key === 1 ? "" : key === undefined ? "s" : "-"+key}`;
|
||
|
value = value ? value.toString() : value;
|
||
|
|
||
|
let check = el => {
|
||
|
let av = el.getAttribute(attr);
|
||
|
return av !== null && (!value || value === av) ? el : null;
|
||
|
}
|
||
|
|
||
|
let checked = check(el);
|
||
|
if (checked)
|
||
|
return checked;
|
||
|
|
||
|
let find = el => {
|
||
|
// Check children first.
|
||
|
for (let child = el.firstElementChild; child; child = child.nextElementSibling) {
|
||
|
let checked = check(child);
|
||
|
if (checked)
|
||
|
return checked;
|
||
|
}
|
||
|
|
||
|
// If no child matches, check children's children.
|
||
|
for (let child = el.firstElementChild; child; child = child.nextElementSibling) {
|
||
|
if (child["rdomCtx"]) // Context change - ignore this branch.
|
||
|
continue;
|
||
|
let found = find(child);
|
||
|
if (found)
|
||
|
return found;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return find(el);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Find all elements with a given key.
|
||
|
* @param {Element} el Element (context) to search inside.
|
||
|
* @param {string | number} key Key to look for.
|
||
|
* @returns {HTMLElement[]} Found elements.
|
||
|
*/
|
||
|
findAll(el, key, value = "", type = "field") {
|
||
|
let attr = `rdom-${type}${key === 1 ? "" : key === undefined ? "s" : "-"+key}`;
|
||
|
value = value ? value.toString() : value;
|
||
|
|
||
|
let all = [];
|
||
|
|
||
|
let check = el => {
|
||
|
let av = el.getAttribute(attr);
|
||
|
return av !== null && (!value || value === av) ? el : null;
|
||
|
}
|
||
|
|
||
|
let checked = check(el);
|
||
|
if (checked)
|
||
|
all.push(checked);
|
||
|
|
||
|
let find = el => {
|
||
|
for (let child = el.firstElementChild; child; child = child.nextElementSibling) {
|
||
|
let checked = check(child);
|
||
|
if (checked)
|
||
|
all.push(checked);
|
||
|
|
||
|
if (child["rdomCtx"]) // Context change - ignore this branch.
|
||
|
continue;
|
||
|
|
||
|
find(child);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
find(el);
|
||
|
return all;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get the context ID of the given element.
|
||
|
* @param {Element} el Element to get the context ID for.
|
||
|
* @returns {string | null} RDOM context ID.
|
||
|
*/
|
||
|
getCtxID(el, self = true) {
|
||
|
let ctx;
|
||
|
|
||
|
if (self && (ctx = el["rdomCtx"]))
|
||
|
return ctx;
|
||
|
|
||
|
for (let /** @type {Element | null} */ p = el; p; p = el.parentElement) {
|
||
|
if (ctx = p["rdomCtx"])
|
||
|
return ctx;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get the context of the given element.
|
||
|
* @param {Element} el Element to get the context for.
|
||
|
* @returns {HTMLElement | null} RDOM context element.
|
||
|
*/
|
||
|
getCtx(el, self = true) {
|
||
|
if (self && el["rdomCtx"])
|
||
|
return /** @type {HTMLElement} */ (el);
|
||
|
|
||
|
for (let p = /** @type {HTMLElement | null} */ (el); p; p = el.parentElement) {
|
||
|
if (p["rdomCtx"])
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
},
|
||
|
|
||
|
/** Prepare an element to be used for RDOM's state-related functions.
|
||
|
* @param {Element} el The element to initialize.
|
||
|
* @returns {HTMLElement} The passed element.
|
||
|
*/
|
||
|
init(el) {
|
||
|
if (el["rdomFields"])
|
||
|
return /** @type {HTMLElement} */ (el);
|
||
|
el["rdomFields"] = {};
|
||
|
el["rdomStates"] = {};
|
||
|
return /** @type {HTMLElement} */ (el);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Move an element to a given index non-destructively.
|
||
|
* @param {ChildNode} el The element to move.
|
||
|
* @param {number} index The target index.
|
||
|
*/
|
||
|
move(el, index) {
|
||
|
if (!el)
|
||
|
return;
|
||
|
|
||
|
let tmp = el;
|
||
|
// @ts-ignore previousElementSibling is too new?
|
||
|
while (tmp = tmp.previousElementSibling)
|
||
|
index--;
|
||
|
|
||
|
// offset == 0: We're fine.
|
||
|
if (!index)
|
||
|
return;
|
||
|
|
||
|
let swap;
|
||
|
tmp = el;
|
||
|
if (index < 0) {
|
||
|
// offset < 0: Element needs to be pushed "left" / "up".
|
||
|
// -offset is the "# of elements we expected there not to be",
|
||
|
// thus how many places we need to shift to the left.
|
||
|
// @ts-ignore previousElementSibling is too new?
|
||
|
while ((swap = tmp) && (tmp = tmp.previousElementSibling) && index < 0)
|
||
|
index++;
|
||
|
swap.before(el);
|
||
|
|
||
|
} else {
|
||
|
// offset > 0: Element needs to be pushed "right" / "down".
|
||
|
// offset is the "# of elements we expected before us but weren't there",
|
||
|
// thus how many places we need to shift to the right.
|
||
|
// @ts-ignore previousElementSibling is too new?
|
||
|
while ((swap = tmp) && (tmp = tmp.nextElementSibling) && index > 0)
|
||
|
index--;
|
||
|
swap.after(el);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/** Escape a string into a HTML - safe format.
|
||
|
* @param {string} m String to escape.
|
||
|
* @returns {string} Escaped string.
|
||
|
*/
|
||
|
escape(m) {
|
||
|
let n = "";
|
||
|
for (let c of ""+m) {
|
||
|
if (c === "&")
|
||
|
n += "&";
|
||
|
else if (c === "<")
|
||
|
n += "<";
|
||
|
else if (c === ">")
|
||
|
n += ">";
|
||
|
else if (c === "\"")
|
||
|
n += """;
|
||
|
else if (c === "'")
|
||
|
n += "'";
|
||
|
else
|
||
|
n += c;
|
||
|
}
|
||
|
return n;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get the holder of the rdom-get with the given value, or all holders into the given object.
|
||
|
* @param {Element} el
|
||
|
* @param {string | any} valueOrObj
|
||
|
* @returns {HTMLElement | any}
|
||
|
*/
|
||
|
get(el, valueOrObj = {}) {
|
||
|
if (typeof(valueOrObj) === "string")
|
||
|
return rdom.find(el, 1, valueOrObj, "get");
|
||
|
|
||
|
for (let field of rdom.findAll(el, 1, "", "get")) {
|
||
|
let key = /** @type {string} */ (field.getAttribute("rdom-get"));
|
||
|
valueOrObj[key] = field;
|
||
|
}
|
||
|
|
||
|
return valueOrObj;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Parse a template string into a HTML string + extra data, escaping expressions unprefixed with $, inserting attribute arrays and preserving child nodes.
|
||
|
* @param {TemplateStringsArray} template
|
||
|
* @param {...any} values
|
||
|
* @returns {RDOMParseResult}
|
||
|
*/
|
||
|
rdparse$(template, ...values) {
|
||
|
try {
|
||
|
let fields = [];
|
||
|
let renderers = [];
|
||
|
let texts = [];
|
||
|
let init = null;
|
||
|
|
||
|
let attrProxies = new Set();
|
||
|
|
||
|
let ignored = 0;
|
||
|
let ids = /** @type {number[]} */ (rdom._cachedIDs.get(template));
|
||
|
if (!ids) {
|
||
|
ids = [];
|
||
|
rdom._cachedIDs.set(template, ids);
|
||
|
}
|
||
|
let idi = -1;
|
||
|
let getid = () => ids[++idi] || (ids[idi] = ++rdom._lastID);
|
||
|
|
||
|
let tag = (tag, attr = "", val = "") => `<rdom-${tag} ${attr}>${val}</rdom-${tag}>`
|
||
|
|
||
|
let html = template.reduce(function rdparse$reduce(prev, next, i) {
|
||
|
let val = values[i - 1];
|
||
|
let t = prev[prev.length - 1];
|
||
|
|
||
|
if (ignored) {
|
||
|
// Ignore val.
|
||
|
--ignored;
|
||
|
return prev + next;
|
||
|
}
|
||
|
|
||
|
if (t === "$") {
|
||
|
// Keep value as-is.
|
||
|
return prev.slice(0, -1) + val + next;
|
||
|
}
|
||
|
|
||
|
if (val && val.key && next.trim() === "=") {
|
||
|
// Settable / gettable field.
|
||
|
next = "";
|
||
|
fields.push({ h: val, key: val.key, state: val.state, value: values[i] });
|
||
|
++ignored;
|
||
|
val = `rdom-field-${rdom.escape(val.key)}="${rdom.escape(val.key)}"`;
|
||
|
|
||
|
} else if (t === "=") {
|
||
|
// Proxy attributes using a field.
|
||
|
if (val && val.join)
|
||
|
val = val.join(" ");
|
||
|
else if (!(val instanceof Function))
|
||
|
val = ""+val;
|
||
|
|
||
|
let split = prev.lastIndexOf(" ") + 1;
|
||
|
let attr = prev.slice(split, -1);
|
||
|
let key = attr;
|
||
|
if (attrProxies.has(key))
|
||
|
key += "-" + getid();
|
||
|
attrProxies.add(attr);
|
||
|
prev = prev.slice(0, split);
|
||
|
let h = rd.attr(key, attr);
|
||
|
h.value = val;
|
||
|
fields.push(h);
|
||
|
val = `rdom-field-${rdom.escape(key)}="${rdom.escape(key)}"`;
|
||
|
|
||
|
} else if (val instanceof Function && i === values.length && !next) {
|
||
|
// Add an init processor, which is present at the end of the template.
|
||
|
init = val;
|
||
|
val = "";
|
||
|
|
||
|
} else if (val && (val instanceof Node || val instanceof Function)) {
|
||
|
// Add placeholders, which will be replaced later on.
|
||
|
let id = getid();
|
||
|
let val_ = val;
|
||
|
renderers.push({ id: id, value: val instanceof Function ? val : () => val_ });
|
||
|
val = tag("empty", "rdom-render="+id);
|
||
|
|
||
|
} else {
|
||
|
// Proxy text using a text node.
|
||
|
let id = getid();
|
||
|
texts.push({ id: id, value: val });
|
||
|
val = tag("text", "rdom-text="+id);
|
||
|
prev = prev.trimEnd();
|
||
|
next = next.trimStart();
|
||
|
}
|
||
|
|
||
|
return prev + val + next;
|
||
|
});
|
||
|
|
||
|
let data = {
|
||
|
fields,
|
||
|
renderers,
|
||
|
texts,
|
||
|
html,
|
||
|
init
|
||
|
};
|
||
|
return data;
|
||
|
} catch (e) {
|
||
|
console.warn("[rdom]", "rd$ failed parsing:", String.raw(template, ...(values.map(v => "${"+v+"}"))), "\n", e);
|
||
|
throw e;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Build the result of rdparse$ into a HTML element.
|
||
|
* @param {Element | null} el
|
||
|
* @param {RDOMParseResult} data
|
||
|
* @returns {HTMLElement}
|
||
|
*/
|
||
|
rdbuild(el, data) {
|
||
|
let elEmpty = null;
|
||
|
if (el && el.tagName === "RDOM-EMPTY") {
|
||
|
elEmpty = el;
|
||
|
el = null;
|
||
|
}
|
||
|
|
||
|
/** @type {Element | null} */
|
||
|
let nodeBase = null;
|
||
|
if (data.html) {
|
||
|
let html = data.html.trim();
|
||
|
nodeBase = /** @type {Element | null} */ (rdom._cachedTemplates.get(html));
|
||
|
if (!nodeBase) {
|
||
|
nodeBase = document.createElement("template");
|
||
|
nodeBase.innerHTML = html;
|
||
|
// @ts-ignore
|
||
|
nodeBase = nodeBase.content.firstElementChild;
|
||
|
rdom._cachedTemplates.set(html, /** @type {Element} */ (nodeBase));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!nodeBase && data.init)
|
||
|
return /** @type {HTMLElement} */ (el || data.init(null));
|
||
|
|
||
|
let init = !el && data.init;
|
||
|
let rel = rdom.init(el || /** @type {Element} */ (document.importNode(/** @type {Element} */ (nodeBase), true)));
|
||
|
|
||
|
if (!rel["rdomCtx"])
|
||
|
rel["rdomCtx"] = ""+(++rdom._lastID);
|
||
|
|
||
|
for (let { id, value } of data.texts) {
|
||
|
let el = rdom.find(rel, 1, id, "text");
|
||
|
if (el && value !== undefined) {
|
||
|
if (el.tagName === "RDOM-TEXT" && el.parentNode?.childNodes.length === 1) {
|
||
|
// Inline rdom-text.
|
||
|
el = /** @type {HTMLElement} */ (el.parentElement);
|
||
|
el.removeChild(el.children[0]);
|
||
|
el.setAttribute("rdom-text", id);
|
||
|
}
|
||
|
el.textContent = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// "Collect" fields.
|
||
|
for (let wrap of data.fields) {
|
||
|
let { h, key, state, value } = wrap;
|
||
|
h = h || wrap;
|
||
|
let el = rdom.init(rdom.find(rel, key));
|
||
|
let fields = el["rdomFields"];
|
||
|
let states = el["rdomStates"];
|
||
|
|
||
|
if (!fields[key]) {
|
||
|
// Initialize the field.
|
||
|
el.setAttribute(
|
||
|
"rdom-fields",
|
||
|
`${el.getAttribute("rdom-fields") || ""} ${key}`.trim()
|
||
|
);
|
||
|
fields[key] = h;
|
||
|
states[key] = state;
|
||
|
if (h.init)
|
||
|
h.init(state, el, key);
|
||
|
}
|
||
|
|
||
|
// Set the value.
|
||
|
if (value !== undefined)
|
||
|
h.set(states[key], el, value);
|
||
|
}
|
||
|
|
||
|
for (let { id, value } of data.renderers) {
|
||
|
let el = rdom.find(rel, 1, id, "render");
|
||
|
if (el && value !== undefined) {
|
||
|
let p = el.parentNode;
|
||
|
if (value && value instanceof Function)
|
||
|
value = value(el.tagName === "RDOM-EMPTY" ? null : el);
|
||
|
value = value || rd$(null)`<rdom-empty/>`;
|
||
|
if (el !== value && !(el.tagName === "RDOM-EMPTY" && value.tagName === "RDOM-EMPTY")) {
|
||
|
// Replace (fill) the field.
|
||
|
p?.replaceChild(value, el);
|
||
|
value.setAttribute("rdom-render", id);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (elEmpty && elEmpty.parentNode)
|
||
|
elEmpty.parentNode.replaceChild(rel, elEmpty);
|
||
|
|
||
|
if (init) {
|
||
|
let rv = init(rel);
|
||
|
if (rv instanceof HTMLElement)
|
||
|
rel = rv || rel;
|
||
|
}
|
||
|
return rel;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Parse a template string into an existing HTML element, escaping expressions unprefixed with $, inserting attribute arrays and preserving child nodes.
|
||
|
* Returns a function parsing a given template string into the given HTML element.
|
||
|
* @param {Element | null} [el]
|
||
|
*/
|
||
|
rd$(el) {
|
||
|
/**
|
||
|
* @param {TemplateStringsArray} template
|
||
|
* @param {any[]} values
|
||
|
*/
|
||
|
function rd$dyn(template, ...values) { return rdbuild(el || null, rdparse$(template, ...values)); }
|
||
|
return rd$dyn;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Parse a template string, escaping expressions unprefixed with $.
|
||
|
* @param {TemplateStringsArray} template
|
||
|
* @param {...any} values
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
escape$(template, ...values) {
|
||
|
return template.reduce(function escape$reduce(prev, next, i) {
|
||
|
let val = values[i - 1];
|
||
|
let t = prev[prev.length - 1];
|
||
|
|
||
|
if (t === "$") {
|
||
|
// Keep value as-is.
|
||
|
return prev.slice(0, -1) + val + next;
|
||
|
}
|
||
|
|
||
|
// Escape HTML
|
||
|
if (val && val.join)
|
||
|
val = val.join(" ");
|
||
|
else
|
||
|
val = rdom.escape(val);
|
||
|
|
||
|
if (t === "=")
|
||
|
// Escape attributes.
|
||
|
val = `"${val}"`;
|
||
|
|
||
|
return prev + val + next;
|
||
|
}).trim();
|
||
|
},
|
||
|
|
||
|
}
|
||
|
|
||
|
export var rdparse$ = rdom.rdparse$;
|
||
|
export var rdbuild = rdom.rdbuild;
|
||
|
export var rd$ = rdom.rd$;
|
||
|
export var escape$ = rdom.escape$;
|
||
|
|
||
|
/**
|
||
|
* Sample RDOM field handlers.
|
||
|
*/
|
||
|
export var rd = {
|
||
|
_: (h, key, state) => {
|
||
|
return {
|
||
|
key: key,
|
||
|
state: state,
|
||
|
init: h.init,
|
||
|
get: h.get,
|
||
|
set: h.set,
|
||
|
};
|
||
|
},
|
||
|
|
||
|
_attr: {
|
||
|
get: (s, el) => s.v,
|
||
|
set: (s, el, v) => {
|
||
|
if (s.v === v)
|
||
|
return;
|
||
|
let prev = s.v;
|
||
|
s.v = v;
|
||
|
if (s.name.startsWith("on") && v instanceof Function) {
|
||
|
let ev = s.name.slice(2);
|
||
|
if (prev && prev instanceof Function)
|
||
|
el.removeEventListener(ev, prev);
|
||
|
el.addEventListener(ev, s.v = v.bind(el), false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
el.setAttribute(s.name, v);
|
||
|
}
|
||
|
},
|
||
|
attr: (key, name) => rd._(rd._attr, key, {
|
||
|
name: name || key,
|
||
|
v: undefined,
|
||
|
}),
|
||
|
|
||
|
_toggleClass: {
|
||
|
get: (s) => s.v,
|
||
|
set: (s, el, v) => {
|
||
|
if (s.v === v)
|
||
|
return;
|
||
|
s.v = v;
|
||
|
if (v) {
|
||
|
el.classList.add(s.nameTrue);
|
||
|
if (s.nameFalse)
|
||
|
el.classList.remove(s.nameFalse);
|
||
|
} else {
|
||
|
el.classList.remove(s.nameTrue);
|
||
|
if (s.nameFalse)
|
||
|
el.classList.add(s.nameFalse);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
toggleClass: (key, nameTrue, nameFalse) => rd._(rd._toggleClass, key, {
|
||
|
nameTrue: nameTrue || key,
|
||
|
nameFalse: nameFalse,
|
||
|
v: undefined,
|
||
|
}),
|
||
|
|
||
|
_html: {
|
||
|
get: (s, el) => s.v,
|
||
|
set: (s, el, v) => {
|
||
|
if (s.v === v)
|
||
|
return;
|
||
|
s.v = v;
|
||
|
el.innerHTML = v;
|
||
|
}
|
||
|
},
|
||
|
html: (key) => rd._(rd._html, key, {
|
||
|
v: undefined,
|
||
|
}),
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A list container context.
|
||
|
*/
|
||
|
export class RDOMListHelper {
|
||
|
/**
|
||
|
* @param {Element} container
|
||
|
*/
|
||
|
constructor(container, ordered = true) {
|
||
|
this.container = container;
|
||
|
this.ordered = ordered;
|
||
|
this._i = -1;
|
||
|
|
||
|
if (container["rdomListHelper"]) {
|
||
|
let ctx = container["rdomListHelper"];
|
||
|
ctx.ordered = ordered;
|
||
|
return ctx;
|
||
|
}
|
||
|
|
||
|
this.container["rdomListHelper"] = this;
|
||
|
|
||
|
/**
|
||
|
* Set of previously added elements.
|
||
|
* This set will be checked against [added] on cleanup, ensuring that any zombies will be removed properly.
|
||
|
* @type {Set<Element>}
|
||
|
*/
|
||
|
this.prev = new Set();
|
||
|
/**
|
||
|
* Set of [rdom.add]ed elements.
|
||
|
* This set will be used and reset in [rdom.cleanup].
|
||
|
* @type {Set<Element>}
|
||
|
*/
|
||
|
this.added = new Set();
|
||
|
|
||
|
/**
|
||
|
* All current element -> object mappings.
|
||
|
* @type {Map<Element, any>}
|
||
|
*/
|
||
|
this.refs = new Map();
|
||
|
/**
|
||
|
* All current object -> element mappings.
|
||
|
* @type {Map<any, Element>}
|
||
|
*/
|
||
|
this.elems = new Map();
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds or updates an element.
|
||
|
* This function needs a reference object so that it can find and update existing elements for any given object.
|
||
|
* @param {any} ref The reference object belonging to the element.
|
||
|
* @param {any} render The element renderer. Either function(Element) : Element, or an object with a property "render" with such a function.
|
||
|
* @returns {HTMLElement} The created / updated wrapper element.
|
||
|
*/
|
||
|
add(ref, render) {
|
||
|
// Check if we already added an element for ref.
|
||
|
// If so, update it. Otherwise create and add a new element.
|
||
|
let el = /** @type {HTMLElement} */ (this.elems.get(ref));
|
||
|
let elOld = el;
|
||
|
el = render.render ? render.render(el) : render(el);
|
||
|
|
||
|
if (elOld) {
|
||
|
if (elOld !== el)
|
||
|
this.container.replaceChild(el, elOld);
|
||
|
} else {
|
||
|
this.container.appendChild(el);
|
||
|
}
|
||
|
|
||
|
if (this.ordered) {
|
||
|
// Move the element to the given index.
|
||
|
rdom.move(el, ++this._i);
|
||
|
}
|
||
|
|
||
|
// Register the element as "added:" - It's not a zombie and won't be removed on cleanup.
|
||
|
this.added.add(el);
|
||
|
// Register the element as the element of ref.
|
||
|
this.refs.set(el, ref);
|
||
|
this.elems.set(ref, el);
|
||
|
return el;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove an element from this context, both the element in the DOM and all references in RDOM.
|
||
|
* @param {Element} el The element to remove.
|
||
|
*/
|
||
|
remove(el) {
|
||
|
if (!el)
|
||
|
return;
|
||
|
let ref = this.refs.get(el);
|
||
|
if (!ref)
|
||
|
return; // The element doesn't belong to this context - no ref object found.
|
||
|
// Remove the element and all related object references from the context.
|
||
|
this.refs.delete(el);
|
||
|
this.elems.delete(ref);
|
||
|
// Remove the element from the DOM.
|
||
|
el.remove();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove zombie elements and perform any other ending cleanup.
|
||
|
* Call this after the last [add].
|
||
|
*/
|
||
|
end() {
|
||
|
for (let el of this.prev) {
|
||
|
if (this.added.has(el))
|
||
|
continue;
|
||
|
this.remove(el);
|
||
|
}
|
||
|
let tmp = this.prev;
|
||
|
this.prev = this.added;
|
||
|
this.added = tmp;
|
||
|
this.added.clear();
|
||
|
this._i = -1;
|
||
|
}
|
||
|
|
||
|
}
|