Something went wrong on our end
Select Git revision
global.html
-
Volker Schukai authoredVolker Schukai authored
updater.mjs 30.78 KiB
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import {internalSymbol} from "../constants.mjs";
import {diff} from "../data/diff.mjs";
import {Pathfinder} from "../data/pathfinder.mjs";
import {Pipe} from "../data/pipe.mjs";
import {
ATTRIBUTE_ERRORMESSAGE,
ATTRIBUTE_UPDATER_ATTRIBUTES,
ATTRIBUTE_UPDATER_BIND,
ATTRIBUTE_UPDATER_BIND_TYPE,
ATTRIBUTE_UPDATER_INSERT,
ATTRIBUTE_UPDATER_INSERT_REFERENCE,
ATTRIBUTE_UPDATER_REMOVE,
ATTRIBUTE_UPDATER_REPLACE,
ATTRIBUTE_UPDATER_SELECT_THIS,
} from "./constants.mjs";
import {Base} from "../types/base.mjs";
import {isArray, isString, isInstance, isIterable} from "../types/is.mjs";
import {Observer} from "../types/observer.mjs";
import {ProxyObserver} from "../types/proxyobserver.mjs";
import {validateArray, validateInstance} from "../types/validate.mjs";
import {clone} from "../util/clone.mjs";
import {trimSpaces} from "../util/trimspaces.mjs";
import {addAttributeToken, addToObjectLink} from "./attributes.mjs";
import {updaterTransformerMethodsSymbol} from "./customelement.mjs";
import {findTargetElementFromEvent} from "./events.mjs";
import {findDocumentTemplate} from "./template.mjs";
import {getWindow} from "./util.mjs";
export {Updater, addObjectWithUpdaterToElement};
/**
* The updater class connects an object with the DOM. In this way, structures and contents in the DOM can be
* programmatically adapted via attributes.
*
* For example, to include a string from an object, the attribute `data-monster-replace` can be used.
* a further explanation can be found under [monsterjs.org](https://monsterjs.org/)
*
* Changes to attributes are made only when the direct values are changed. If you want to assign changes
* to other values as well, you have to insert the attribute `data-monster-select-this`. This should be
* done with care, as it can reduce performance.
*
* @example /examples/libraries/dom/updater/simple/ Simple example
*
* @license AGPLv3
* @since 1.8.0
* @copyright schukai GmbH
* @throws {Error} the value is not iterable
* @throws {Error} pipes are not allowed when cloning a node.
* @throws {Error} no template was found with the specified key.
* @throws {Error} the maximum depth for the recursion is reached.
* @throws {TypeError} value is not a object
* @throws {TypeError} value is not an instance of HTMLElement
* @summary The updater class connects an object with the dom
*/
class Updater extends Base {
/**
* @since 1.8.0
* @param {HTMLElement} element
* @param {object|ProxyObserver|undefined} subject
* @throws {TypeError} value is not a object
* @throws {TypeError} value is not an instance of HTMLElement
* @see {@link findDocumentTemplate}
*/
constructor(element, subject) {
super();
/**
* @type {HTMLElement}
*/
if (subject === undefined) subject = {};
if (!isInstance(subject, ProxyObserver)) {
subject = new ProxyObserver(subject);
}
this[internalSymbol] = {
element: validateInstance(element, HTMLElement),
last: {},
callbacks: new Map(),
eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"],
subject: subject,
};
this[internalSymbol].callbacks.set(
"checkstate",
getCheckStateCallback.call(this),
);
this[internalSymbol].subject.attachObserver(
new Observer(() => {
const s = this[internalSymbol].subject.getRealSubject();
const diffResult = diff(this[internalSymbol].last, s);
this[internalSymbol].last = clone(s);
const promises = [];
for (const [, change] of Object.entries(diffResult)) {
promises.push(
new Promise((resolve, reject) => {
getWindow().requestAnimationFrame(() => {
try {
removeElement.call(this, change);
insertElement.call(this, change);
updateContent.call(this, change);
updateAttributes.call(this, change);
resolve();
} catch (error) {
reject(error);
}
});
}),
);
}
return Promise.all(promises);
}),
);
}
/**
* Defaults: 'keyup', 'click', 'change', 'drop', 'touchend'
*
* @see {@link https://developer.mozilla.org/de/docs/Web/Events}
* @since 1.9.0
* @param {Array} types
* @return {Updater}
*/
setEventTypes(types) {
this[internalSymbol].eventTypes = validateArray(types);
return this;
}
/**
* With this method, the eventlisteners are hooked in and the magic begins.
*
* ```js
* updater.run().then(() => {
* updater.enableEventProcessing();
* });
* ```
*
* @since 1.9.0
* @return {Updater}
* @throws {Error} the bind argument must start as a value with a path
*/
enableEventProcessing() {
this.disableEventProcessing();
for (const type of this[internalSymbol].eventTypes) {
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
this[internalSymbol].element.addEventListener(
type,
getControlEventHandler.call(this),
{
capture: true,
passive: true,
},
);
}
return this;
}
/**
* This method turns off the magic or who loves it more profane it removes the eventListener.
*
* @since 1.9.0
* @return {Updater}
*/
disableEventProcessing() {
for (const type of this[internalSymbol].eventTypes) {
this[internalSymbol].element.removeEventListener(
type,
getControlEventHandler.call(this),
);
}
return this;
}
/**
* The run method must be called for the update to start working.
* The method ensures that changes are detected.
*
* ```js
* updater.run().then(() => {
* updater.enableEventProcessing();
* });
* ```
*
* @summary Let the magic begin
* @return {Promise}
*/
run() {
// the key __init__has no further meaning and is only
// used to create the diff for empty objects.
this[internalSymbol].last = {__init__: true};
return this[internalSymbol].subject.notifyObservers();
}
/**
* Gets the values of bound elements and changes them in subject
*
* @since 1.27.0
* @return {Monster.DOM.Updater}
*/
retrieve() {
retrieveFromBindings.call(this);
return this;
}
/**
* If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here.
* However, if you passed a simple object, here you will get a proxy for that object.
*
* For changes, the ProxyObserver must be used.
*
* @since 1.8.0
* @return {Proxy}
*/
getSubject() {
return this[internalSymbol].subject.getSubject();
}
/**
* This method can be used to register commands that can be called via call: instruction.
* This can be used to provide a pipe with its own functionality.
*
* @param {string} name
* @param {function} callback
* @return {Transformer}
* @throws {TypeError} value is not a string
* @throws {TypeError} value is not a function
*/
setCallback(name, callback) {
this[internalSymbol].callbacks.set(name, callback);
return this;
}
}
/**
* @private
* @license AGPLv3
* @since 1.9.0
* @return {function
* @this Updater
*/
function getCheckStateCallback() {
return function (current) {
// this is a reference to the current object (therefore no array function here)
if (this instanceof HTMLInputElement) {
if (["radio", "checkbox"].indexOf(this.type) !== -1) {
return `${this.value}` === `${current}` ? "true" : undefined;
}
} else if (this instanceof HTMLOptionElement) {
if (isArray(current) && current.indexOf(this.value) !== -1) {
return "true";
}
return undefined;
}
};
}
/**
* @private
*/
const symbol = Symbol("@schukai/monster/updater@@EventHandler");
/**
* @private
* @return {function}
* @this Updater
* @throws {Error} the bind argument must start as a value with a path
*/
function getControlEventHandler() {
if (this[symbol]) {
return this[symbol];
}
/**
* @throws {Error} the bind argument must start as a value with a path.
* @throws {Error} unsupported object
* @param {Event} event
*/
this[symbol] = (event) => {
const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND);
if (element === undefined) {
return;
}
queueMicrotask(() => {
try {
retrieveAndSetValue.call(this, element);
} catch (e) {
addAttributeToken(element, ATTRIBUTE_ERRORMESSAGE, e.message || `${e}`);
}
});
};
return this[symbol];
}
/**
* @throws {Error} the bind argument must start as a value with a path
* @param {HTMLElement} element
* @return void
* @private
*/
function retrieveAndSetValue(element) {
const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject());
let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND);
if (path === null)
throw new Error("the bind argument must start as a value with a path");
if (path.indexOf("path:") !== 0) {
throw new Error("the bind argument must start as a value with a path");
}
path = path.substring(5); // remove path: from the string
let value;
if (element instanceof HTMLInputElement) {
switch (element.type) {
case "checkbox":
value = element.checked ? element.value : undefined;
break;
default:
value = element.value;
break;
}
} else if (element instanceof HTMLTextAreaElement) {
value = element.value;
} else if (element instanceof HTMLSelectElement) {
switch (element.type) {
case "select-one":
value = element.value;
break;
case "select-multiple":
value = element.value;
let options = element?.selectedOptions;
if (options === undefined)
options = element.querySelectorAll(":scope option:checked");
value = Array.from(options).map(({value}) => value);
break;
}
// values from custom elements
} else if (
(element?.constructor?.prototype &&
!!Object.getOwnPropertyDescriptor(
element.constructor.prototype,
"value",
)?.["get"]) ||
element.hasOwnProperty("value")
) {
value = element?.["value"];
} else {
throw new Error("unsupported object");
}
if (isString(value)) {
const type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE);
switch (type) {
case "number":
case "int":
case "float":
case "integer":
value = Number(value);
if (isNaN(value)) {
value = 0;
}
break;
case "boolean":
case "bool":
case "checkbox":
value = value === "true" || value === "1" || value === "on";
break;
case "string[]":
value = value.split(",").map((v) => `${v}`);
break;
case "int[]":
case "integer[]":
if (value === "") {
value = [];
} else {
value = value.split(",").map((v) => {
try {
return parseInt(v, 10);
} catch (e) {
}
return -1;
}).filter((v) => v !== -1);
}
break
case "[]":
case "array":
case "list":
value = value.split(",");
break;
case "object":
case "json":
value = JSON.parse(value);
break;
default:
break;
}
}
const copy = clone(this[internalSymbol].subject.getRealSubject());
const pf = new Pathfinder(copy);
pf.setVia(path, value);
const diffResult = diff(copy, this[internalSymbol].subject.getRealSubject());
if (diffResult.length > 0) {
pathfinder.setVia(path, value);
}
}
/**
* @license AGPLv3
* @since 1.27.0
* @return void
* @private
*/
function retrieveFromBindings() {
if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) {
retrieveAndSetValue.call(this, this[internalSymbol].element);
}
for (const [, element] of this[internalSymbol].element
.querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`)
.entries()) {
retrieveAndSetValue.call(this, element);
}
}
/**
* @private
* @license AGPLv3
* @since 1.8.0
* @param {object} change
* @return {void}
*/
function removeElement(change) {
for (const [, element] of this[internalSymbol].element
.querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`)
.entries()) {
element.parentNode.removeChild(element);
}
}
/**
* @private
* @license AGPLv3
* @since 1.8.0
* @param {object} change
* @return {void}
* @throws {Error} the value is not iterable
* @throws {Error} pipes are not allowed when cloning a node.
* @throws {Error} no template was found with the specified key.
* @throws {Error} the maximum depth for the recursion is reached.
* @this Updater
*/
function insertElement(change) {
const subject = this[internalSymbol].subject.getRealSubject();
const mem = new WeakSet();
let wd = 0;
const container = this[internalSymbol].element;
while (true) {
let found = false;
wd++;
const p = clone(change?.["path"]);
if (!isArray(p)) return;
while (p.length > 0) {
const current = p.join(".");
let iterator = new Set();
const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`;
const e = container.querySelectorAll(query);
if (e.length > 0) {
iterator = new Set([...e]);
}
if (container.matches(query)) {
iterator.add(container);
}
for (const [, containerElement] of iterator.entries()) {
if (mem.has(containerElement)) continue;
mem.add(containerElement);
found = true;
const attributes = containerElement.getAttribute(
ATTRIBUTE_UPDATER_INSERT,
);
if (attributes === null) continue;
const def = trimSpaces(attributes);
const i = def.indexOf(" ");
const key = trimSpaces(def.substr(0, i));
const refPrefix = `${key}-`;
const cmd = trimSpaces(def.substr(i));
// this case is actually excluded by the query but is nevertheless checked again here
if (cmd.indexOf("|") > 0) {
throw new Error("pipes are not allowed when cloning a node.");
}
const pipe = new Pipe(cmd);
this[internalSymbol].callbacks.forEach((f, n) => {
pipe.setCallback(n, f);
});
let value;
try {
containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
value = pipe.run(subject);
} catch (e) {
containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
}
const dataPath = cmd.split(":").pop();
let insertPoint;
if (containerElement.hasChildNodes()) {
insertPoint = containerElement.lastChild;
}
if (!isIterable(value)) {
throw new Error("the value is not iterable");
}
const available = new Set();
for (const [i] of Object.entries(value)) {
const ref = refPrefix + i;
const currentPath = `${dataPath}.${i}`;
available.add(ref);
const refElement = containerElement.querySelector(
`[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`,
);
if (refElement instanceof HTMLElement) {
insertPoint = refElement;
continue;
}
appendNewDocumentFragment(containerElement, key, ref, currentPath);
}
const nodes = containerElement.querySelectorAll(
`[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`,
);
for (const [, node] of Object.entries(nodes)) {
if (
!available.has(
node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE),
)
) {
try {
containerElement.removeChild(node);
} catch (e) {
containerElement.setAttribute(
ATTRIBUTE_ERRORMESSAGE,
`${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${
e.message
}`.trim(),
);
}
}
}
}
p.pop();
}
if (found === false) break;
if (wd++ > 200) {
throw new Error("the maximum depth for the recursion is reached.");
}
}
}
/**
*
* @private
* @license AGPLv3
* @since 1.8.0
* @param {HTMLElement} container
* @param {string} key
* @param {string} ref
* @param {string} path
* @throws {Error} no template was found with the specified key.
*/
function appendNewDocumentFragment(container, key, ref, path) {
const template = findDocumentTemplate(key, container);
const nodes = template.createDocumentFragment();
for (const [, node] of Object.entries(nodes.childNodes)) {
if (node instanceof HTMLElement) {
applyRecursive(node, key, path);
node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref);
}
container.appendChild(node);
}
}
/**
* @private
* @license AGPLv3
* @since 1.10.0
* @param {HTMLElement} node
* @param {string} key
* @param {string} path
* @return {void}
*/
function applyRecursive(node, key, path) {
if (node instanceof HTMLElement) {
if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) {
const value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
node.setAttribute(
ATTRIBUTE_UPDATER_REPLACE,
value.replaceAll(`path:${key}`, `path:${path}`),
);
}
if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
node.setAttribute(
ATTRIBUTE_UPDATER_ATTRIBUTES,
value.replaceAll(`path:${key}`, `path:${path}`),
);
}
for (const [, child] of Object.entries(node.childNodes)) {
applyRecursive(child, key, path);
}
}
}
/**
* @private
* @license AGPLv3
* @since 1.8.0
* @param {object} change
* @return {void}
* @this Updater
*/
function updateContent(change) {
const subject = this[internalSymbol].subject.getRealSubject();
const p = clone(change?.["path"]);
runUpdateContent.call(this, this[internalSymbol].element, p, subject);
const slots = this[internalSymbol].element.querySelectorAll("slot");
if (slots.length > 0) {
for (const [, slot] of Object.entries(slots)) {
for (const [, element] of Object.entries(slot.assignedNodes())) {
runUpdateContent.call(this, element, p, subject);
}
}
}
}
/**
* @private
* @license AGPLv3
* @since 1.8.0
* @param {HTMLElement} container
* @param {array} parts
* @param {object} subject
* @return {void}
*/
function runUpdateContent(container, parts, subject) {
if (!isArray(parts)) return;
if (!(container instanceof HTMLElement)) return;
parts = clone(parts);
const mem = new WeakSet();
while (parts.length > 0) {
const current = parts.join(".");
parts.pop();
// Unfortunately, static data is always changed as well, since it is not possible to react to changes here.
const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`;
const e = container.querySelectorAll(`${query}`);
const iterator = new Set([...e]);
if (container.matches(query)) {
iterator.add(container);
}
/**
* @type {HTMLElement}
*/
for (const [element] of iterator.entries()) {
if (mem.has(element)) return;
mem.add(element);
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
const cmd = trimSpaces(attributes);
const pipe = new Pipe(cmd);
this[internalSymbol].callbacks.forEach((f, n) => {
pipe.setCallback(n, f);
});
let value;
try {
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
value = pipe.run(subject);
} catch (e) {
element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
}
if (value instanceof HTMLElement) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
try {
element.appendChild(value);
} catch (e) {
element.setAttribute(
ATTRIBUTE_ERRORMESSAGE,
`${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${
e.message
}`.trim(),
);
}
} else {
element.innerHTML = value;
}
}
}
}
/**
* @private
* @since 1.8.0
* @param {object} change
* @return {void}
*/
function updateAttributes(change) {
const subject = this[internalSymbol].subject.getRealSubject();
const p = clone(change?.["path"]);
runUpdateAttributes.call(this, this[internalSymbol].element, p, subject);
}
/**
* @private
* @param {HTMLElement} container
* @param {array} parts
* @param {object} subject
* @return {void}
* @this Updater
*/
function runUpdateAttributes(container, parts, subject) {
if (!isArray(parts)) return;
parts = clone(parts);
const mem = new WeakSet();
while (parts.length > 0) {
const current = parts.join(".");
parts.pop();
let iterator = new Set();
const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`;
const e = container.querySelectorAll(query);
if (e.length > 0) {
iterator = new Set([...e]);
}
if (container.matches(query)) {
iterator.add(container);
}
for (const [element] of iterator.entries()) {
if (mem.has(element)) return;
mem.add(element);
// this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set
if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
continue;
}
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
for (let [, def] of Object.entries(attributes.split(","))) {
def = trimSpaces(def);
const i = def.indexOf(" ");
const name = trimSpaces(def.substr(0, i));
const cmd = trimSpaces(def.substr(i));
const pipe = new Pipe(cmd);
this[internalSymbol].callbacks.forEach((f, n) => {
pipe.setCallback(n, f, element);
});
let value;
try {
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
value = pipe.run(subject);
} catch (e) {
element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
}
if (value === undefined) {
element.removeAttribute(name);
} else if (element.getAttribute(name) !== value) {
element.setAttribute(name, value);
}
handleInputControlAttributeUpdate.call(this, element, name, value);
}
}
}
}
/**
* @private
* @param {HTMLElement|*} element
* @param {string} name
* @param {string|number|undefined} value
* @return {void}
* @this Updater
*/
function handleInputControlAttributeUpdate(element, name, value) {
if (element instanceof HTMLSelectElement) {
switch (element.type) {
case "select-multiple":
for (const [index, opt] of Object.entries(element.options)) {
opt.selected = value.indexOf(opt.value) !== -1;
}
break;
case "select-one":
// Only one value may be selected
for (const [index, opt] of Object.entries(element.options)) {
if (opt.value === value) {
element.selectedIndex = index;
break;
}
}
break;
}
} else if (element instanceof HTMLInputElement) {
switch (element.type) {
case "radio":
if (name === "checked") {
element.checked = value !== undefined;
}
break;
case "checkbox":
if (name === "checked") {
element.checked = value !== undefined;
}
break;
case "text":
default:
if (name === "value") {
element.value = value === undefined ? "" : value;
}
break;
}
} else if (element instanceof HTMLTextAreaElement) {
if (name === "value") {
element.value = value === undefined ? "" : value;
}
}
}
/**
* @param {NodeList|HTMLElement|Set<HTMLElement>} elements
* @param {Symbol} symbol
* @param {object} object
* @param {object} config
*
* Config: enableEventProcessing {boolean} - default: false - enables the event processing
*
* @return {Promise[]}
* @license AGPLv3
* @since 1.23.0
* @throws {TypeError} elements is not an instance of NodeList, HTMLElement or Set
* @throws {TypeError} the context of the function is not an instance of HTMLElement
* @throws {TypeError} symbol must be an instance of Symbol
*/
function addObjectWithUpdaterToElement(elements, symbol, object, config = {}) {
if (!(this instanceof HTMLElement)) {
throw new TypeError(
"the context of this function must be an instance of HTMLElement",
);
}
if (!(typeof symbol === "symbol")) {
throw new TypeError("symbol must be an instance of Symbol");
}
const updaters = new Set();
if (elements instanceof NodeList) {
elements = new Set([...elements]);
} else if (elements instanceof HTMLElement) {
elements = new Set([elements]);
} else if (elements instanceof Set) {
} else {
throw new TypeError(
`elements is not a valid type. (actual: ${typeof elements})`,
);
}
const result = [];
const updaterCallbacks = [];
const cb = this?.[updaterTransformerMethodsSymbol];
if (this instanceof HTMLElement && typeof cb === "function") {
const callbacks = cb.call(this);
if (typeof callbacks === "object") {
for (const [name, callback] of Object.entries(callbacks)) {
if (typeof callback === "function") {
updaterCallbacks.push([name, callback]);
} else {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
`onUpdaterPipeCallbacks: ${name} is not a function`,
);
}
}
} else {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
`onUpdaterPipeCallbacks do not return an object with functions`,
);
}
}
elements.forEach((element) => {
if (!(element instanceof HTMLElement)) return;
if (element instanceof HTMLTemplateElement) return;
const u = new Updater(element, object);
updaters.add(u);
if (updaterCallbacks.length > 0) {
for (const [name, callback] of updaterCallbacks) {
u.setCallback(name, callback);
}
}
result.push(
u.run().then(() => {
if (config.eventProcessing === true) {
u.enableEventProcessing();
}
return u;
}),
);
});
if (updaters.size > 0) {
addToObjectLink(this, symbol, updaters);
}
return result;
}