hydro-js
Version:
A lightweight reactive library
1,317 lines • 54.5 kB
JavaScript
// Safari Polyfills
window.requestIdleCallback =
/* c8 ignore next 4 */
window.requestIdleCallback ||
((cb, _, start = window.performance.now()) => window.setTimeout(cb, 0, {
didTimeout: false,
timeRemaining: () => Math.max(0, 5 - (window.performance.now() - start)),
}));
// Safari Polyfills END
const range = document.createRange();
range.selectNodeContents(range.createContextualFragment(`<${"template" /* Placeholder.template */}>`).lastChild);
const parser = range.createContextualFragment.bind(range);
const allNodeChanges = new WeakMap(); // Maps a Node against an array of changes. An array is necessary because a node can have multiple variables for one text / attribute.
const elemEventFunctions = new WeakMap(); // Stores event functions in order to compare Elements against each other.
const reactivityMap = new WeakMap(); // Maps Proxy Objects to another Map(proxy-key, node).
const bindMap = new WeakMap(); // Bind an Element to data. If the data is being unset, the DOM Element disappears too.
const tmpSwap = new WeakMap(); // Take over keyToNodeMap if the new value is a hydro Proxy. Save old reactivityMap entry here, in case for a swap operation.
const onRenderMap = new WeakMap(); // Lifecycle Hook that is being called after rendering
const onCleanupMap = new WeakMap(); // Lifecycle Hook that is being called when unmount function is being called
const fragmentToElements = new WeakMap(); // Used to retreive Elements from DocumentFragment after it has been rendered – for diffing
const hydroToReactive = new WeakMap(); // Used for internal mapping from hydroKeys to the the Proxy created by the reactive function
const _boundFunctions = Symbol("boundFunctions"); // Cache for bound functions in Proxy, so that we create the bound version of each function only once
const reactiveSymbol = Symbol("reactive");
const keysSymbol = Symbol("keys");
const viewElementsEventFunctions = new Map();
const isServerSideCached = isServerSide();
let globalSchedule = true; // Decides whether to schedule rendering and updating (async)
let reuseElements = true; // Reuses Elements when rendering
let insertBeforeDiffing = false; // Makes sense in Chrome only
let shouldSetReactivity = true;
let viewElements = false;
let ignoreIsConnected = false;
const reactivityRegex = new RegExp(isServerSideCached
? `\\{\\{([^]*?)\\}\\}|${"hydro-reactive-" /* Placeholder.reactiveKey */}([a-zA-Z0-9_.-]+)`
: `\\{\\{([^]*?)\\}\\}`);
const HTML_FIND_INVALID = /<(\/?)(html|head|body)(>|\s.*?>)/g;
const newLineRegex = /\n/g;
const propChainRegex = /[\.\[\]]/;
const onEventRegex = /^on/;
// https://html.spec.whatwg.org/#attributes-3
// if value for bool attr is falsy, then remove attr
// INFO: draggable and spellcheck are actually using booleans as string! Also, hidden is not really a bool attr, but is making use of the empty string too. Might consider to add 'translate' (yes and no as string)
const boolAttrSet = new Set([
"allowfullscreen",
"alpha",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"draggable",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootcustomelementregistry",
"shadowrootdelegatesfocus",
"shadowrootserializable",
"spellcheck",
]);
let lastSwapElem = null;
let internReset = false;
const primitiveTypes = new Set([
"number",
"string",
"symbol",
"boolean",
"bigint",
]);
function isObject(obj) {
return obj != null && typeof obj === "object";
}
function isFunction(func) {
return typeof func === "function" /* Placeholder.function */;
}
function isTextNode(node) {
return node.splitText !== undefined;
}
function isNode(node) {
return node instanceof window.Node;
}
function isDocumentFragment(node) {
return node.nodeType === 11;
}
function isEventObject(obj) {
return (isObject(obj) && "event" /* Placeholder.event */ in obj && "options" /* Placeholder.options */ in obj);
}
function isProxy(hydroObject) {
return Reflect.get(hydroObject, "isProxy" /* Placeholder.isProxy */);
}
function isPromise(obj) {
return isObject(obj) && typeof obj.then === "function";
}
function isServerSide() {
return (window.navigator.userAgent.includes("Node.js") ||
window.navigator.userAgent.includes("Deno") ||
window.navigator.userAgent.includes("Bun") ||
window.navigator.userAgent.includes("HappyDOM") ||
window.navigator.userAgent.includes("jsdom"));
}
function randomText() {
const randomChars = "abcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < 6; i++) {
result += randomChars.charAt(Math.floor(Math.random() * randomChars.length));
}
return result;
// return Math.random().toString(32).slice(2);
}
function setGlobalSchedule(willSchedule) {
globalSchedule = willSchedule;
setHydroRecursive(hydro);
}
function setReuseElements(willReuse) {
reuseElements = willReuse;
}
function setInsertDiffing(willInsert) {
insertBeforeDiffing = willInsert;
}
function setShouldSetReactivity(willSet) {
shouldSetReactivity = willSet;
}
function setIgnoreIsConnected(ignore) {
ignoreIsConnected = ignore;
}
function setHydroRecursive(obj) {
Reflect.set(obj, "asyncUpdate" /* Placeholder.asyncUpdate */, globalSchedule);
for (const value of Object.values(obj)) {
if (isObject(value) && isProxy(value)) {
setHydroRecursive(value);
}
}
}
function setAttribute(node, key, val) {
const isBoolAttr = boolAttrSet.has(key);
if (isBoolAttr && !val) {
node.removeAttribute(key);
return false;
}
node.setAttribute(key, isFunction(val) && Reflect.has(val, reactiveSymbol)
? val
: isBoolAttr
? ""
: val);
return true;
}
function addEventListener(node, eventName, obj) {
node.addEventListener(eventName, isFunction(obj) ? obj : obj.event, isFunction(obj) ? {} : obj.options);
}
function html(htmlArray, ...variables) {
const eventFunctions = {}; // Temporarily store a mapping for string -> function, because eventListener have to be registered after the Element's creation
let insertNodes = []; // Nodes, that will be added after the parsing
const resolvedVariables = Array.from({
length: variables.length,
});
for (let i = 0; i < variables.length; i++) {
const variable = variables[i];
const template = `<${"template" /* Placeholder.template */} id="lbInsertNodes"></${"template" /* Placeholder.template */}>`;
if (isNode(variable)) {
insertNodes.push(variable);
resolvedVariables[i] = template;
}
else if (primitiveTypes.has(typeof variable) ||
Reflect.has(variable, reactiveSymbol)) {
resolvedVariables[i] = String(variable);
}
else if (isFunction(variable) || isEventObject(variable)) {
const funcName = randomText();
Reflect.set(eventFunctions, funcName, variable);
viewElements &&
Reflect.set(viewElementsEventFunctions, funcName, variable);
resolvedVariables[i] = funcName;
}
else if (Array.isArray(variable)) {
for (let index = 0; index < variable.length; index++) {
const item = variable[index];
if (isNode(item)) {
insertNodes.push(item);
variable[index] = template;
}
}
resolvedVariables[i] = variable.join("");
}
else if (isObject(variable)) {
let result = "";
for (const [key, value] of Object.entries(variable)) {
if (isFunction(value) || isEventObject(value)) {
const funcName = randomText();
Reflect.set(eventFunctions, funcName, value);
viewElements &&
Reflect.set(viewElementsEventFunctions, funcName, value);
result += `${key}="${funcName}"`;
}
else {
result += `${key}="${value}"`;
}
}
resolvedVariables[i] = result;
}
}
// Find elements <html|head|body>, as they cannot be created by the parser. Replace them by fake Custom Elements and replace them afterwards.
let DOMString = String.raw(htmlArray, ...resolvedVariables).trim();
DOMString = DOMString.replace(HTML_FIND_INVALID, `<$1$2${"-dummy" /* Placeholder.dummy */}$3`);
const DOM = parser(DOMString);
// Delay Element iteration and manipulation after the elements have been added to the DOM.
if (!viewElements) {
fillDOM(DOM, insertNodes, eventFunctions);
}
// Return DocumentFragment
if (DOM.childNodes.length > 1)
return DOM;
// Return empty Text Node
if (!DOM.firstChild)
return document.createTextNode("");
// Return Element | Text
return DOM.firstChild;
}
function fillDOM(elem, insertNodes, eventFunctions) {
const root = document.createNodeIterator(elem, window.NodeFilter.SHOW_ELEMENT, {
acceptNode(element) {
return element.localName.endsWith("-dummy" /* Placeholder.dummy */)
? window.NodeFilter.FILTER_ACCEPT
: window.NodeFilter.FILTER_REJECT;
},
});
const nodes = [];
let currentNode;
while ((currentNode = root.nextNode())) {
nodes.push(currentNode);
}
for (const node of nodes) {
const tag = node.localName.replace("-dummy" /* Placeholder.dummy */, "");
const replacement = document.createElement(tag);
/* c8 ignore next 3 */
for (const key of node.getAttributeNames()) {
replacement.setAttribute(key, node.getAttribute(key));
}
replacement.append(...node.childNodes);
node.replaceWith(replacement);
}
// Insert HTML Elements, which were stored in insertNodes
if (!isTextNode(elem)) {
for (const template of elem.querySelectorAll("template[id^=lbInsertNodes]"))
template.replaceWith(insertNodes.shift());
}
if (shouldSetReactivity)
setReactivity(elem, eventFunctions);
}
function h(name, props, ...children) {
if (isFunction(name))
return name({ ...props, children });
const elem = typeof name === "string" /* Placeholder.string */
? document.createElement(name, props?.hasOwnProperty("is") ? { is: props["is"] } : undefined)
: document.createDocumentFragment();
for (const i in props) {
i in elem && !boolAttrSet.has(i)
? //@ts-ignore
(elem[i] = props[i])
: setAttribute(elem, i, props[i]);
}
if (isDocumentFragment(elem)) {
children = name.children;
}
elem.append(...(children.some((i) => Array.isArray(i))
? children.map(getChildren).flat()
: children));
if (!viewElements) {
setReactivity(elem);
}
return elem;
}
function getChildren(child) {
return isObject(child) && !isNode(child)
? Object.values(child)
: child;
}
/* c8 ignore end */
function setReactivity(DOM, eventFunctions) {
if (isTextNode(DOM)) {
setReactivitySingle(DOM);
return;
}
const elems = document.createNodeIterator(DOM, window.NodeFilter.SHOW_ELEMENT);
let elem;
while ((elem = elems.nextNode())) {
for (const key of elem.getAttributeNames()) {
// Set functions
const val = elem.getAttribute(key);
if (eventFunctions && key.startsWith("on")) {
const eventName = key.replace(onEventRegex, "");
const event = Reflect.get(eventFunctions, val);
if (!event) {
setReactivitySingle(elem, key, val);
continue;
}
elem.removeAttribute(key);
if (isEventObject(event)) {
elem.addEventListener(eventName, event.event, event.options);
if (elemEventFunctions.has(elem)) {
elemEventFunctions.get(elem).push(event.event);
}
else {
elemEventFunctions.set(elem, [event.event]);
}
}
else {
elem.addEventListener(eventName, event);
if (elemEventFunctions.has(elem)) {
elemEventFunctions.get(elem).push(event);
}
else {
elemEventFunctions.set(elem, [event]);
}
}
}
else {
setReactivitySingle(elem, key, val);
}
}
let childNode = elem.firstChild;
while (childNode) {
if (isTextNode(childNode) &&
(childNode.nodeValue?.includes("{{") ||
(isServerSideCached &&
childNode.nodeValue?.includes("hydro-reactive-" /* Placeholder.reactiveKey */)))) {
setReactivitySingle(childNode);
}
childNode = childNode.nextSibling;
}
}
}
function setReactivitySingle(node, key, val) {
let attr_OR_text, match;
if (!key) {
attr_OR_text = node.nodeValue; // nodeValue is (always) defined on Text Nodes
}
else {
attr_OR_text = val;
if (attr_OR_text === "") {
// e.g. checked attribute or two-way attribute
attr_OR_text = key;
if (attr_OR_text.startsWith("{{") ||
(isServerSideCached && attr_OR_text.startsWith("hydro-reactive-" /* Placeholder.reactiveKey */))) {
node.removeAttribute(attr_OR_text);
}
}
}
while ((match = attr_OR_text.match(reactivityRegex))) {
// attr_OR_text will be altered in every iteration
const [hydroMatch, hydroCurlyPath, hydroPath] = match;
const properties = (hydroCurlyPath ?? hydroPath)
.trim()
.replace(newLineRegex, "")
.split(propChainRegex)
.filter(Boolean);
const [resolvedValue, resolvedObj] = resolveObject(properties);
let lastProp = properties[properties.length - 1];
const start = match.index;
let end = start + String(resolvedValue).length;
if (isNode(resolvedValue)) {
node.nodeValue = attr_OR_text.replace(hydroMatch, "");
node.after(resolvedValue);
setTraces(start, end, resolvedValue, lastProp, resolvedObj, key);
return;
}
// Set Text or set Attribute
if (isTextNode(node)) {
const textContent = isObject(resolvedValue)
? window.JSON.stringify(resolvedValue)
: resolvedValue ?? "";
attr_OR_text = attr_OR_text.replace(hydroMatch, textContent);
if (attr_OR_text != null) {
node.nodeValue = attr_OR_text;
}
}
else {
if (key === "bind") {
attr_OR_text = attr_OR_text.replace(hydroMatch, "");
node.removeAttribute(key);
const proxy = isObject(resolvedValue) && isProxy(resolvedValue)
? resolvedValue
: resolvedObj;
if (bindMap.has(proxy)) {
bindMap.get(proxy).push(node);
}
else {
bindMap.set(proxy, [node]);
}
continue;
}
else if (key === "two-way" /* Placeholder.twoWay */) {
if (node instanceof window.HTMLSelectElement) {
node.value = resolvedValue;
changeAttrVal("change" /* Placeholder.change */, node, resolvedObj, lastProp);
}
else if (node instanceof window.HTMLInputElement &&
node.type === "radio" /* Placeholder.radio */) {
node.checked = node.value === resolvedValue;
changeAttrVal("change" /* Placeholder.change */, node, resolvedObj, lastProp);
}
else if (node instanceof window.HTMLInputElement &&
node.type === "checkbox" /* Placeholder.checkbox */) {
node.checked = resolvedValue;
changeAttrVal("change" /* Placeholder.change */, node, resolvedObj, lastProp, true);
}
else if (node instanceof window.HTMLTextAreaElement ||
node instanceof window.HTMLInputElement) {
node.value = resolvedValue;
changeAttrVal("input", node, resolvedObj, lastProp);
}
attr_OR_text = attr_OR_text.replace(hydroMatch, "");
node.toggleAttribute("two-way" /* Placeholder.twoWay */);
}
else if (isFunction(resolvedValue) || isEventObject(resolvedValue)) {
attr_OR_text = attr_OR_text.replace(hydroMatch, "");
node.removeAttribute(key);
addEventListener(node, key.replace(onEventRegex, ""), resolvedValue);
}
else if (isObject(resolvedValue)) {
// Case: setting attrs on Element - <p ${props}>
for (const [subKey, subVal] of Object.entries(resolvedValue)) {
attr_OR_text = attr_OR_text.replace(hydroMatch, "");
if (isFunction(subVal) || isEventObject(subVal)) {
addEventListener(node, subKey.replace(onEventRegex, ""), subVal);
}
else {
lastProp = subKey;
if (setAttribute(node, subKey, subVal)) {
end = start + String(subVal).length;
}
else {
end = start;
}
}
setTraces(start, end, node, lastProp, resolvedValue, subKey);
}
continue; // As we set all Mappings via subKeys
}
else {
attr_OR_text = attr_OR_text.replace(hydroMatch, resolvedValue);
if (!setAttribute(node, key, attr_OR_text === String(resolvedValue)
? resolvedValue
: attr_OR_text)) {
attr_OR_text = attr_OR_text.replace(resolvedValue, "");
}
}
}
setTraces(start, end, node, lastProp, resolvedObj, key);
}
}
// Same behavior as v-model in https://v3.vuejs.org/guide/forms.html#basic-usage
function changeAttrVal(eventName, node, resolvedObj, lastProp, isChecked = false) {
node.addEventListener(eventName, changeHandler);
onCleanup(() => node.removeEventListener(eventName, changeHandler), node);
function changeHandler({ target }) {
Reflect.set(resolvedObj, lastProp, isChecked
? target.checked
: target.value);
}
}
function setTraces(start, end, node, hydroKey, resolvedObj, key) {
// Set WeakMaps, that will be used to track a change for a Node but also to check if a Node has any other changes.
const change = [start, end, key, resolvedObj];
const changeArr = [change];
if (allNodeChanges.has(node)) {
allNodeChanges.get(node).push(change);
}
else {
allNodeChanges.set(node, [change]); // Use own version. Otherwise changes, will lead to incorrect changes in the DOM.
}
if (reactivityMap.has(resolvedObj)) {
const keyToNodeMap = reactivityMap.get(resolvedObj);
const nodeToChangeMap = keyToNodeMap.get(hydroKey);
if (nodeToChangeMap) {
if (nodeToChangeMap.has(node)) {
nodeToChangeMap.get(node).push(change);
}
else {
nodeToChangeMap.set(changeArr, node);
nodeToChangeMap.set(node, changeArr);
}
}
else {
keyToNodeMap.set(hydroKey,
//@ts-ignore
new Map([
[changeArr, node],
[node, changeArr],
]));
}
}
else {
reactivityMap.set(resolvedObj, new Map([
[
hydroKey,
//@ts-ignore
new Map([
[changeArr, node],
[node, changeArr],
]),
],
]));
}
}
// Helper function to return a value and hydro obj from a chain of properties
function resolveObject(propertyArray) {
let value, prev;
value = prev = hydro;
for (const prop of propertyArray) {
prev = value;
value = Reflect.get(prev, prop);
}
return [value, prev];
}
function compareEvents(elem, where, onlyTextChildren) {
const elemFunctions = [];
const whereFunctions = [];
if (isTextNode(elem)) {
if (onRenderMap.has(elem))
elemFunctions.push(onRenderMap.get(elem));
if (onCleanupMap.has(elem))
elemFunctions.push(onCleanupMap.get(elem));
if (onRenderMap.has(where))
whereFunctions.push(onRenderMap.get(where));
if (onCleanupMap.has(where))
whereFunctions.push(onCleanupMap.get(where));
return (elemFunctions.length === whereFunctions.length &&
String(elemFunctions) === String(whereFunctions));
}
if (elemEventFunctions.has(elem)) {
elemFunctions.push(...elemEventFunctions.get(elem));
}
if (elemEventFunctions.has(where)) {
whereFunctions.push(...elemEventFunctions.get(where));
}
if (onRenderMap.has(elem))
elemFunctions.push(onRenderMap.get(elem));
if (onCleanupMap.has(elem))
elemFunctions.push(onCleanupMap.get(elem));
if (onRenderMap.has(where))
whereFunctions.push(onRenderMap.get(where));
if (onCleanupMap.has(where))
whereFunctions.push(onCleanupMap.get(where));
if (elemFunctions.length !== whereFunctions.length)
return false;
if (String(elemFunctions) !== String(whereFunctions))
return false;
for (let i = 0; i < elem.childNodes.length; i++) {
const elemChild = elem.childNodes[i];
const whereChild = where.childNodes[i];
if (onlyTextChildren) {
if (isTextNode(elemChild)) {
if (!compareEvents(elemChild, whereChild, onlyTextChildren)) {
return false;
}
}
}
else if (!compareEvents(elemChild, whereChild)) {
return false;
}
}
return true;
}
function compare(elem, where, onlyTextChildren) {
if (isDocumentFragment(elem) || isDocumentFragment(where))
return false;
return (elem.isEqualNode(where) && compareEvents(elem, where, onlyTextChildren));
}
function render(elem, where = "", shouldSchedule = globalSchedule) {
/* c8 ignore next 4 */
if (shouldSchedule) {
schedule(render, elem, where, false);
return unmount(elem);
}
// Get elem value if elem is reactiveObject
if (Reflect.has(elem, reactiveSymbol)) {
elem = getValue(elem);
}
// Store elements of documentFragment for later unmount
let elemChildren = [];
if (isDocumentFragment(elem)) {
elemChildren = Array.from(elem.childNodes);
fragmentToElements.set(elem, elemChildren); // For diffing later
}
if (!where) {
document.body.append(elem);
}
else {
if (typeof where === "string" /* Placeholder.string */) {
const resolveStringToElement = $(where);
if (resolveStringToElement) {
where = resolveStringToElement;
}
else {
return noop;
}
}
if (!reuseElements) {
replaceElement(elem, where);
}
else {
if (isTextNode(elem)) {
replaceElement(elem, where);
}
else if (!compare(elem, where)) {
treeDiff(elem, where);
}
}
}
runLifecyle(elem, onRenderMap);
for (const subElem of elemChildren) {
runLifecyle(subElem, onRenderMap);
}
return unmount(isDocumentFragment(elem) ? elemChildren : elem);
}
function noop() { }
function executeLifecycle(node, lifecyleMap) {
if (lifecyleMap.has(node)) {
const fn = lifecyleMap.get(node);
if (globalSchedule) {
schedule(fn);
}
else {
fn();
}
lifecyleMap.delete(node);
}
}
function runLifecyle(node, lifecyleMap) {
if ((lifecyleMap === onRenderMap && !calledOnRender) ||
(lifecyleMap === onCleanupMap && !calledOnCleanup))
return;
executeLifecycle(node, lifecyleMap);
const elements = document.createNodeIterator(node, window.NodeFilter.SHOW_ELEMENT);
let subElem;
while ((subElem = elements.nextNode())) {
executeLifecycle(subElem, lifecyleMap);
let childNode = subElem.firstChild;
while (childNode) {
if (isTextNode(childNode)) {
executeLifecycle(childNode, lifecyleMap);
}
childNode = childNode.nextSibling;
}
}
}
function filterTag2Elements(tag2Elements, root) {
for (const [localName, list] of tag2Elements.entries()) {
// Process list in reverse to avoid index issues when splicing
for (let i = list.length - 1; i >= 0; i--) {
const elem = list[i];
if (root.contains(elem) || root.isSameNode(elem)) {
list.splice(i, 1);
}
}
if (list.length === 0) {
tag2Elements.delete(localName);
}
}
}
function treeDiff(elem, where) {
const elemElements = [...elem.querySelectorAll("*")];
if (!isDocumentFragment(elem))
elemElements.unshift(elem);
let whereElements = [];
if (!isTextNode(where)) {
whereElements = [...where.querySelectorAll("*")];
if (!isDocumentFragment(where))
whereElements.unshift(where);
}
let template;
if (insertBeforeDiffing) {
template = document.createElement(isServerSideCached ? "div" : "template");
/* c8 ignore next 3 */
if (where === document.documentElement) {
where.append(template);
}
else {
if (isDocumentFragment(where)) {
fragmentToElements.get(where)[0].before(template);
}
else {
where.before(template);
}
}
template.append(elem);
}
// Create Mapping for easier diffing, eg: "div" -> [...Element]
const tag2Elements = new Map();
for (const wElem of whereElements) {
/* c8 ignore next 2 */
if (insertBeforeDiffing && wElem === template)
return;
if (tag2Elements.has(wElem.localName)) {
tag2Elements.get(wElem.localName).push(wElem);
}
else {
tag2Elements.set(wElem.localName, [wElem]);
}
}
// Re-use any where Element if possible, then remove elem Element
for (const subElem of elemElements) {
const sameElements = tag2Elements.get(subElem.localName);
if (sameElements) {
for (const whereElem of sameElements) {
if (compare(subElem, whereElem, true)) {
subElem.replaceWith(whereElem);
runLifecyle(subElem, onCleanupMap);
filterTag2Elements(tag2Elements, whereElem);
break;
}
}
}
}
if (insertBeforeDiffing) {
const newElems = isDocumentFragment(elem)
? Array.from(template.childNodes)
: [elem];
if (isDocumentFragment(where)) {
const oldElems = fragmentToElements.get(where);
for (const e of newElems)
oldElems[0].before(e);
for (const e of oldElems)
e.remove();
}
else {
if (where instanceof window.HTMLHtmlElement) {
replaceElement(elem, where);
}
else {
where.replaceWith(...newElems);
}
}
template.remove();
runLifecyle(where, onCleanupMap);
}
else {
replaceElement(elem, where);
}
tag2Elements.clear();
}
function replaceElement(elem, where) {
if (isDocumentFragment(where)) {
const fragmentChildren = fragmentToElements.get(where);
if (isDocumentFragment(elem)) {
const fragmentElements = Array.from(elem.childNodes);
for (let index = 0; index < fragmentChildren.length; index++) {
const fragWhere = fragmentChildren[index];
if (index < fragmentElements.length) {
render(fragmentElements[index], fragWhere);
}
else {
fragWhere.remove();
}
}
}
else {
for (let index = 0; index < fragmentChildren.length; index++) {
const fragWhere = fragmentChildren[index];
if (index === 0) {
render(elem, fragWhere);
}
else {
fragWhere.remove();
}
}
}
}
else if (isServerSideCached) {
if (isServerSideCached &&
elem instanceof window.HTMLHtmlElement &&
where instanceof window.HTMLHtmlElement) {
for (const key of elem.getAttributeNames()) {
setAttribute(where, key, elem.getAttribute(key));
}
where.replaceChildren(...elem.childNodes);
}
else {
where.replaceWith(elem);
}
}
else {
where.replaceWith(elem);
}
runLifecyle(where, onCleanupMap);
}
function unmount(elem) {
if (Array.isArray(elem)) {
return () => elem.forEach(removeElement);
}
else {
return () => removeElement(elem);
}
}
function removeElement(elem) {
if (!ignoreIsConnected && elem.isConnected) {
elem.remove();
runLifecyle(elem, onCleanupMap);
}
}
/* c8 ignore next 13 */
async function schedule(fn, ...args) {
if ("scheduler" in window) {
// @ts-ignore
window.scheduler.postTask(() => fn(...args), { priority: "user-blocking" });
}
else {
window.requestIdleCallback(() => fn(...args));
}
}
function reactive(initial) {
let key;
do
key = randomText();
while (Reflect.has(hydro, key));
Reflect.set(hydro, key, initial);
Reflect.set(setter, reactiveSymbol, true);
const chainKeysProxy = chainKeys(setter, [key]);
if (isObject(initial)) {
hydroToReactive.set(Reflect.get(hydro, key), chainKeysProxy);
}
return chainKeysProxy;
function setter(val) {
const keys = // @ts-ignore
(this && Reflect.has(this, reactiveSymbol) ? this : chainKeysProxy)[keysSymbol.description];
const [resolvedValue, resolvedObj] = resolveObject(keys);
const lastProp = keys[keys.length - 1];
if (isFunction(val)) {
const returnVal = val(resolvedValue);
const sameObject = resolvedValue === returnVal;
if (sameObject)
return;
Reflect.set(resolvedObj, lastProp, returnVal ?? resolvedValue);
}
else {
Reflect.set(resolvedObj, lastProp, val);
}
}
}
function chainKeys(initial, keys) {
return new Proxy(initial, {
get(target, subKey, _receiver) {
if (subKey === reactiveSymbol.description)
return true;
if (subKey === keysSymbol.description) {
return keys;
}
if (subKey === Symbol.toPrimitive) {
return () => isServerSideCached
? `${"hydro-reactive-" /* Placeholder.reactiveKey */}${keys.join(".")}`
: `{{${keys.join(".")}}}`;
}
return chainKeys(target, [...keys, subKey]);
},
});
}
function getReactiveKeys(reactiveHydro) {
const keys = reactiveHydro[keysSymbol.description];
const lastProp = keys[keys.length - 1];
return [lastProp, keys.length === 1];
}
function unset(reactiveHydro) {
const [lastProp, oneKey] = getReactiveKeys(reactiveHydro);
if (oneKey) {
Reflect.set(hydro, lastProp, null);
if (hydroToReactive.has(hydro[lastProp])) {
hydroToReactive.delete(hydro[lastProp]);
}
}
else {
const [_, resolvedObj] = resolveObject(reactiveHydro[keysSymbol.description]);
Reflect.set(resolvedObj, lastProp, null);
}
}
function setAsyncUpdate(reactiveHydro, asyncUpdate) {
const [_, oneKey] = getReactiveKeys(reactiveHydro);
if (oneKey) {
hydro.asyncUpdate = asyncUpdate;
}
else {
const [_, resolvedObj] = resolveObject(reactiveHydro[keysSymbol.description]);
resolvedObj.asyncUpdate = asyncUpdate;
}
}
function observe(reactiveHydro, fn) {
const [lastProp, oneKey] = getReactiveKeys(reactiveHydro);
if (oneKey) {
hydro.observe(lastProp, fn);
}
else {
const [_, resolvedObj] = resolveObject(reactiveHydro[keysSymbol.description]);
resolvedObj.observe(lastProp, fn);
}
}
function unobserve(reactiveHydro) {
const [lastProp, oneKey] = getReactiveKeys(reactiveHydro);
if (oneKey) {
hydro.unobserve(lastProp);
}
else {
const [_, resolvedObj] = resolveObject(reactiveHydro[keysSymbol.description]);
resolvedObj.unobserve(lastProp);
}
}
function ternary(condition, trueVal, falseVal, reactiveHydro = condition) {
const checkCondition = (cond) => (!Reflect.has(condition, reactiveSymbol) && isFunction(condition)
? condition(cond)
: isPromise(cond)
? false
: cond)
? isFunction(trueVal)
? trueVal()
: trueVal
: isFunction(falseVal)
? falseVal()
: falseVal;
const ternaryValue = reactive(checkCondition(getValue(reactiveHydro)));
observe(reactiveHydro, (newVal) => {
newVal === null
? unset(ternaryValue)
: ternaryValue(checkCondition(newVal));
});
return ternaryValue;
}
function emit(eventName, data, who, options = { bubbles: true }) {
who.dispatchEvent(new window.CustomEvent(eventName, { ...options, detail: data }));
}
let trackDeps = false;
const trackProxies = new Set();
const trackMap = new WeakMap();
const unobserveMap = new WeakMap();
function watchEffect(fn) {
trackDeps = true;
fn();
trackDeps = false;
const reRun = (newVal) => {
if (newVal !== null)
fn();
};
for (const proxy of trackProxies) {
if (!trackMap.has(proxy))
continue;
for (const key of trackMap.get(proxy)) {
proxy.observe(key, reRun);
if (unobserveMap.has(reRun)) {
unobserveMap.get(reRun).push({ proxy, key });
}
else {
unobserveMap.set(reRun, [{ proxy, key }]);
}
}
trackMap.delete(proxy);
}
trackProxies.clear();
return () => unobserveMap
.get(reRun)
.forEach((entry) => entry.proxy.unobserve(entry.key, reRun));
}
function getValue(reactiveHydro) {
const [resolvedValue] = resolveObject(Reflect.get(reactiveHydro, keysSymbol.description));
return resolvedValue;
}
let calledOnRender = false;
function onRender(fn, elem, ...args) {
calledOnRender = true;
onRenderMap.set(elem, args.length ? fn.bind(fn, ...args) : fn);
}
let calledOnCleanup = false;
function onCleanup(fn, elem, ...args) {
calledOnCleanup = true;
onCleanupMap.set(elem, args.length ? fn.bind(fn, ...args) : fn);
}
// Core of the library
function generateProxy(obj) {
const handlers = Symbol("handlers"); // For observer pattern
const boundFunctions = new WeakMap();
const proxy = new Proxy(obj ?? {}, {
// If receiver is a getter, then it is the object on which the search first started for the property|key -> Proxy
set(target, key, val, receiver) {
if (trackDeps) {
trackProxies.add(receiver);
if (trackMap.has(receiver)) {
trackMap.get(receiver).add(key);
}
else {
trackMap.set(receiver, new Set([key]));
}
}
let returnSet = true;
let oldVal = Reflect.get(target, key, receiver);
if (oldVal === val)
return returnSet;
// Reset Path - mostly GC
if (val === null) {
// Remove entry from reactitivyMap underlying Map
if (reactivityMap.has(receiver)) {
const key2NodeMap = reactivityMap.get(receiver);
key2NodeMap.delete(String(key));
if (key2NodeMap.size === 0) {
reactivityMap.delete(receiver);
}
}
// Inform the Observers about null change and unobserve
const observer = Reflect.get(target, handlers, receiver);
if (observer.has(key)) {
let set = observer.get(key);
for (const handler of set) {
handler(null, oldVal);
}
set.clear();
receiver.unobserve(key);
}
// If oldVal is a Proxy - clean it
if (isObject(oldVal) && isProxy(oldVal)) {
reactivityMap.delete(oldVal);
if (bindMap.has(oldVal)) {
bindMap.get(oldVal).forEach(removeElement);
bindMap.delete(oldVal);
}
}
else {
if (bindMap.has(receiver)) {
bindMap.get(receiver).forEach(removeElement);
bindMap.delete(receiver);
}
}
// Remove item from array
/* c8 ignore next 4 */
if (!internReset && Array.isArray(receiver)) {
receiver.splice(Number(key), 1);
return returnSet;
}
return Reflect.deleteProperty(receiver, key);
}
// Set the value
if (isPromise(val)) {
val
.then((value) => {
// No Reflect in order to trigger the Getter
receiver[key] = value;
})
.catch((e) => {
console.error(e);
receiver[key] = null;
});
returnSet = Reflect.set(target, key, val, receiver);
return returnSet;
}
else if (isNode(val)) {
returnSet = Reflect.set(target, key, val, receiver);
}
else if (isObject(val) && !isProxy(val)) {
returnSet = Reflect.set(target, key, generateProxy(val), receiver);
// Recursively set properties to Proxys too
for (const [subKey, subVal] of Object.entries(val)) {
if (isObject(subVal) && !isProxy(subVal)) {
Reflect.set(val, subKey, generateProxy(subVal));
}
}
}
else {
if (!reuseElements &&
Array.isArray(receiver) &&
receiver.includes(oldVal) &&
receiver.includes(val) &&
/* c8 ignore start */
bindMap.has(val)) {
const [elem] = bindMap.get(val);
if (lastSwapElem !== elem) {
const [oldElem] = bindMap.get(oldVal);
lastSwapElem = oldElem;
const prevElem = elem.previousSibling;
const prevOldElem = oldElem.previousSibling;
// Move it in the array too without triggering the proxy set
const index = receiver.findIndex((i) => i === val);
receiver.splice(Number(key), 1, val);
receiver.splice(index, 1, oldVal);
prevElem.after(oldElem);
prevOldElem.after(elem);
}
return true;
}
else {
/* c8 ignore end */
returnSet = Reflect.set(target, key, val, receiver);
}
}
const newVal = Reflect.get(target, key, receiver);
// Check if DOM needs to be updated
// oldVal can be Proxy value too
if (reactivityMap.has(oldVal)) {
checkReactivityMap(oldVal, key, newVal, oldVal);
}
else if (reactivityMap.has(receiver)) {
checkReactivityMap(receiver, key, newVal, oldVal);
}
// current val (before setting) is a proxy - take over its keyToNodeMap
if (isObject(val) && isProxy(val)) {
if (reactivityMap.has(oldVal)) {
// Store old reactivityMap if it is a swap operation
reuseElements && tmpSwap.set(oldVal, reactivityMap.get(oldVal));
if (tmpSwap.has(val)) {
reactivityMap.set(oldVal, tmpSwap.get(val));
tmpSwap.delete(val);
}
else {
reactivityMap.set(oldVal, reactivityMap.get(val));
}
}
}
// Inform the Observers
if (returnSet) {
Reflect.get(target, handlers, receiver)
.get(key)
?.forEach((handler) => handler(newVal, oldVal));
}
// If oldVal is a Proxy - clean it
!reuseElements && oldVal && cleanProxy(oldVal);
return returnSet;
},
// fix proxy bugs, e.g Map
get(target, prop, receiver) {
if (trackDeps) {
trackProxies.add(receiver);
if (trackMap.has(receiver)) {
trackMap.get(receiver).add(prop);
}
else {
trackMap.set(receiver, new Set([prop]));
}
}
const value = Reflect.get(target, prop, receiver);
if (!isFunction(value)) {
return value;
}
if (!boundFunctions.has(value)) {
boundFunctions.set(value, value.bind(target));
}
return boundFunctions.get(value);
},
});
Reflect.defineProperty(proxy, "isProxy" /* Placeholder.isProxy */, {
value: true,
});
Reflect.defineProperty(proxy, "asyncUpdate" /* Placeholder.asyncUpdate */, {
value: globalSchedule,
writable: true,
});
Reflect.defineProperty(proxy, handlers, {
value: new Map(),
});
Reflect.defineProperty(proxy, "observe" /* Placeholder.observe */, {
value(key, handler) {
const map = Reflect.get(proxy, handlers);
if (map.has(key)) {
map.get(key).add(handler);
}
else {
map.set(key, new Set([handler]));
}
},
configurable: true,
});
Reflect.defineProperty(proxy, "getObservers" /* Placeholder.getObservers */, {
value() {
return Reflect.get(proxy, handlers);
},
configurable: true,
});
Reflect.defineProperty(proxy, "unobserve" /* Placeholder.unobserve */, {
value(key, handler) {
const map = Reflect.get(proxy, handlers);
if (key) {
if (map.has(key)) {
if (handler == null) {
map.delete(key);
}
else {
const set = map.get(key);
if (set?.has(handler)) {
set.delete(handler);
}
}
}
/* c8 ignore next 3 */
}
else {
map.clear();
}
},
configurable: true,
});
if (!obj)
Reflect.defineProperty(proxy, _boundFunctions, {
value: boundFunctions,
});
return proxy;
}
function cleanProxy(proxy) {
if (isObject(proxy) && isProxy(proxy)) {
reactivityMap.delete(proxy);
/* c8 ignore next 4 */
if (bindMap.has(proxy)) {
bindMap.get(proxy).forEach(removeElement);
bindMap.delete(proxy);
}
}
}
function checkReactivityMap(obj, key, val, oldVal) {
const keyToNodeMap = reactivityMap.get(obj);
const nodeToChangeMap = keyToNodeMap.get(String(key));
if (nodeToChangeMap) {
/* c8 ignore next 5 */
if (Reflect.get(obj, "asyncUpdate" /* Placeholder.asyncUpdate */)) {
schedule(updateDOM, nodeToChangeMap, val, oldVal);
}
else {
updateDOM(nodeToChangeMap, val, oldVal);
}
}
if (isObject(val)) {
const entries = Object.entries(val);
for (const [subKey, subVal] of entries) {
const subOldVal = (isObject(oldVal) && Reflect.get(oldVal, subKey)) || oldVal;
const nodeToChangeMap = keyToNodeMap.get(subKey);
if (nodeToChangeMap) {
/* c8 ignore next 5 */
if (Reflect.get(obj, "asyncUpdate" /* Placeholder.asyncUpdate */)) {
schedule(updateDOM, nodeToChangeMap, subVal, subOldVal);
}
else {
updateDOM(nodeToChangeMap, subVal, subOldVal);
}
}
}
}
}
function updateDOM(nodeToChangeMap, val, oldVal) {
nodeToChangeMap.forEach((entry) => {
// Circular reference in order to keep Memory low
if (isNode(entry)) {
/* c8 ignore next 5 */
if (!ignoreIsConnected && !entry.isConnected) {
const tmpChange = nodeToChangeMap.get(entry);
nodeToChangeMap.delete(entry);
nodeToChangeMap.delete(tmpChange);
}
return; // Continue in forEach
}
// For each change of the node update either attribute or textContent
for (const change of entry) {
const node = nodeToChangeMap.get(entry);
const [start, end, key] = change;
let useStartEnd = false;
if (isNode(val) && (!isServerSideCached || val !== node)) {
replaceElement(val, node);
if (isServerSideCached || val !== node) {
nodeToChangeMap.delete(node);
if (!isDocumentFragment(val)) {
nodeToChangeMap.set(val, entry);
nodeToChangeMap.set(entry, val);
}
}
}
else if (isTextNode(node)) {
useStartEnd = true;
let text = node.nodeValue;
node.nodeValue =
text.substring(0, start) + String(val) + text.substring(end);
}
else {
if (key === "two-way" /* Placeholder.twoWay */) {
if (node instanceof window.HTMLInputElement &&
node.type === "radio" /* Placeholder.radio */) {
node.checked = Array.isArray(val)
? val.includes(node.name)
: String(val) === node.value;
}
else if (node instanceof window.HTMLInputElement &&
node.type === "checkbox" /* Placeholder.checkbox */) {
node.checked = val;
}
else if (node instanceof window.HTMLTextAreaElement ||
node instanceof window.HTMLSelectElement ||
node instanceof window.HTMLInputElement) {
node.value = String(val);
}
}
else if (isFunction(val) || isEventObject(val)) {
const eventName = key.replace(onEventRegex, "");
node.removeEventListener(eventName, isFunction(oldVal) ? oldVal : oldVal.event);
addEventListener(node, eventName, val);
}
else if (isObject(val)) {
const entries = Object.entries(val);
for (const [subKey, subVal] of entries) {
if (isFunction(subVal) || isEventObject(subVal)) {
const eventName = subKey.replace(onEventRegex, "");
node.removeEventListener(eventName, isFunction(oldVal[subKey])
? oldVal[subKey]
: oldVal[subKey].eve