UNPKG

hono

Version:

Web framework built on Web Standards

579 lines (578 loc) 17.8 kB
// src/jsx/dom/render.ts import { toArray } from "../children.js"; import { DOM_ERROR_HANDLER, DOM_INTERNAL_TAG, DOM_RENDERER, DOM_STASH } from "../constants.js"; import { globalContexts as globalJSXContexts, useContext } from "../context.js"; import { STASH_EFFECT } from "../hooks/index.js"; import { normalizeIntrinsicElementKey, styleObjectForEach } from "../utils.js"; import { createContext } from "./context.js"; var HONO_PORTAL_ELEMENT = "_hp"; var eventAliasMap = { Change: "Input", DoubleClick: "DblClick" }; var nameSpaceMap = { svg: "2000/svg", math: "1998/Math/MathML" }; var buildDataStack = []; var refCleanupMap = /* @__PURE__ */ new WeakMap(); var nameSpaceContext = void 0; var getNameSpaceContext = () => nameSpaceContext; var isNodeString = (node) => "t" in node; var eventCache = { onClick: ["click", false] }; var getEventSpec = (key) => { if (!key.startsWith("on")) { return void 0; } if (eventCache[key]) { return eventCache[key]; } const match = key.match(/^on([A-Z][a-zA-Z]+?(?:PointerCapture)?)(Capture)?$/); if (match) { const [, eventName, capture] = match; return eventCache[key] = [(eventAliasMap[eventName] || eventName).toLowerCase(), !!capture]; } return void 0; }; var toAttributeName = (element, key) => nameSpaceContext && element instanceof SVGElement && /[A-Z]/.test(key) && (key in element.style || key.match(/^(?:o|pai|str|u|ve)/)) ? key.replace(/([A-Z])/g, "-$1").toLowerCase() : key; var applyProps = (container, attributes, oldAttributes) => { attributes ||= {}; for (let key in attributes) { const value = attributes[key]; if (key !== "children" && (!oldAttributes || oldAttributes[key] !== value)) { key = normalizeIntrinsicElementKey(key); const eventSpec = getEventSpec(key); if (eventSpec) { if (oldAttributes?.[key] !== value) { if (oldAttributes) { container.removeEventListener(eventSpec[0], oldAttributes[key], eventSpec[1]); } if (value != null) { if (typeof value !== "function") { throw new Error(`Event handler for "${key}" is not a function`); } container.addEventListener(eventSpec[0], value, eventSpec[1]); } } } else if (key === "dangerouslySetInnerHTML" && value) { container.innerHTML = value.__html; } else if (key === "ref") { let cleanup; if (typeof value === "function") { cleanup = value(container) || (() => value(null)); } else if (value && "current" in value) { value.current = container; cleanup = () => value.current = null; } refCleanupMap.set(container, cleanup); } else if (key === "style") { const style = container.style; if (typeof value === "string") { style.cssText = value; } else { style.cssText = ""; if (value != null) { styleObjectForEach(value, style.setProperty.bind(style)); } } } else { if (key === "value") { const nodeName = container.nodeName; if (nodeName === "INPUT" || nodeName === "TEXTAREA" || nodeName === "SELECT") { ; container.value = value === null || value === void 0 || value === false ? null : value; if (nodeName === "TEXTAREA") { container.textContent = value; continue; } else if (nodeName === "SELECT") { if (container.selectedIndex === -1) { ; container.selectedIndex = 0; } continue; } } } else if (key === "checked" && container.nodeName === "INPUT" || key === "selected" && container.nodeName === "OPTION") { ; container[key] = value; } const k = toAttributeName(container, key); if (value === null || value === void 0 || value === false) { container.removeAttribute(k); } else if (value === true) { container.setAttribute(k, ""); } else if (typeof value === "string" || typeof value === "number") { container.setAttribute(k, value); } else { container.setAttribute(k, value.toString()); } } } } if (oldAttributes) { for (let key in oldAttributes) { const value = oldAttributes[key]; if (key !== "children" && !(key in attributes)) { key = normalizeIntrinsicElementKey(key); const eventSpec = getEventSpec(key); if (eventSpec) { container.removeEventListener(eventSpec[0], value, eventSpec[1]); } else if (key === "ref") { refCleanupMap.get(container)?.(); } else { container.removeAttribute(toAttributeName(container, key)); } } } } }; var invokeTag = (context, node) => { node[DOM_STASH][0] = 0; buildDataStack.push([context, node]); const func = node.tag[DOM_RENDERER] || node.tag; const props = func.defaultProps ? { ...func.defaultProps, ...node.props } : node.props; try { return [func.call(null, props)]; } finally { buildDataStack.pop(); } }; var getNextChildren = (node, container, nextChildren, childrenToRemove, callbacks) => { if (node.vR?.length) { childrenToRemove.push(...node.vR); delete node.vR; } if (typeof node.tag === "function") { node[DOM_STASH][1][STASH_EFFECT]?.forEach((data) => callbacks.push(data)); } node.vC.forEach((child) => { if (isNodeString(child)) { nextChildren.push(child); } else { if (typeof child.tag === "function" || child.tag === "") { child.c = container; const currentNextChildrenIndex = nextChildren.length; getNextChildren(child, container, nextChildren, childrenToRemove, callbacks); if (child.s) { for (let i = currentNextChildrenIndex; i < nextChildren.length; i++) { nextChildren[i].s = true; } child.s = false; } } else { nextChildren.push(child); if (child.vR?.length) { childrenToRemove.push(...child.vR); delete child.vR; } } } }); }; var findInsertBefore = (node) => { for (; ; node = node.tag === HONO_PORTAL_ELEMENT || !node.vC || !node.pP ? node.nN : node.vC[0]) { if (!node) { return null; } if (node.tag !== HONO_PORTAL_ELEMENT && node.e) { return node.e; } } }; var removeNode = (node) => { if (!isNodeString(node)) { node[DOM_STASH]?.[1][STASH_EFFECT]?.forEach((data) => data[2]?.()); refCleanupMap.get(node.e)?.(); if (node.p === 2) { node.vC?.forEach((n) => n.p = 2); } node.vC?.forEach(removeNode); } if (!node.p) { node.e?.remove(); delete node.e; } if (typeof node.tag === "function") { updateMap.delete(node); fallbackUpdateFnArrayMap.delete(node); delete node[DOM_STASH][3]; node.a = true; } }; var apply = (node, container, isNew) => { node.c = container; applyNodeObject(node, container, isNew); }; var findChildNodeIndex = (childNodes, child) => { if (!child) { return; } for (let i = 0, len = childNodes.length; i < len; i++) { if (childNodes[i] === child) { return i; } } return; }; var cancelBuild = Symbol(); var applyNodeObject = (node, container, isNew) => { const next = []; const remove = []; const callbacks = []; getNextChildren(node, container, next, remove, callbacks); remove.forEach(removeNode); const childNodes = isNew ? void 0 : container.childNodes; let offset; if (isNew) { offset = -1; } else { offset = (childNodes.length && (findChildNodeIndex(childNodes, findInsertBefore(node.nN)) ?? findChildNodeIndex( childNodes, next.find((n) => n.tag !== HONO_PORTAL_ELEMENT && n.e)?.e ))) ?? -1; if (offset === -1) { isNew = true; } } for (let i = 0, len = next.length; i < len; i++, offset++) { const child = next[i]; let el; if (child.s && child.e) { el = child.e; child.s = false; } else { const isNewLocal = isNew || !child.e; if (isNodeString(child)) { if (child.e && child.d) { child.e.textContent = child.t; } child.d = false; el = child.e ||= document.createTextNode(child.t); } else { el = child.e ||= child.n ? document.createElementNS(child.n, child.tag) : document.createElement(child.tag); applyProps(el, child.props, child.pP); applyNodeObject(child, el, isNewLocal); } } if (child.tag === HONO_PORTAL_ELEMENT) { offset--; } else if (isNew) { if (!el.parentNode) { container.appendChild(el); } } else if (childNodes[offset] !== el && childNodes[offset - 1] !== el) { if (childNodes[offset + 1] === el) { container.appendChild(childNodes[offset]); } else { container.insertBefore(el, childNodes[offset] || null); } } } if (node.pP) { delete node.pP; } if (callbacks.length) { const useLayoutEffectCbs = []; const useEffectCbs = []; callbacks.forEach(([, useLayoutEffectCb, , useEffectCb, useInsertionEffectCb]) => { if (useLayoutEffectCb) { useLayoutEffectCbs.push(useLayoutEffectCb); } if (useEffectCb) { useEffectCbs.push(useEffectCb); } useInsertionEffectCb?.(); }); useLayoutEffectCbs.forEach((cb) => cb()); if (useEffectCbs.length) { requestAnimationFrame(() => { useEffectCbs.forEach((cb) => cb()); }); } } }; var fallbackUpdateFnArrayMap = /* @__PURE__ */ new WeakMap(); var build = (context, node, children) => { const buildWithPreviousChildren = !children && node.pC; if (children) { node.pC ||= node.vC; } let foundErrorHandler; try { children ||= typeof node.tag == "function" ? invokeTag(context, node) : toArray(node.props.children); if (children[0]?.tag === "" && children[0][DOM_ERROR_HANDLER]) { foundErrorHandler = children[0][DOM_ERROR_HANDLER]; context[5].push([context, foundErrorHandler, node]); } const oldVChildren = buildWithPreviousChildren ? [...node.pC] : node.vC ? [...node.vC] : void 0; const vChildren = []; let prevNode; for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { children.splice(i, 1, ...children[i].flat()); } let child = buildNode(children[i]); if (child) { if (typeof child.tag === "function" && !child.tag[DOM_INTERNAL_TAG]) { if (globalJSXContexts.length > 0) { child[DOM_STASH][2] = globalJSXContexts.map((c) => [c, c.values.at(-1)]); } if (context[5]?.length) { child[DOM_STASH][3] = context[5].at(-1); } } let oldChild; if (oldVChildren && oldVChildren.length) { const i2 = oldVChildren.findIndex( isNodeString(child) ? (c) => isNodeString(c) : child.key !== void 0 ? (c) => c.key === child.key && c.tag === child.tag : (c) => c.tag === child.tag ); if (i2 !== -1) { oldChild = oldVChildren[i2]; oldVChildren.splice(i2, 1); } } if (oldChild) { if (isNodeString(child)) { if (oldChild.t !== child.t) { ; oldChild.t = child.t; oldChild.d = true; } child = oldChild; } else { const pP = oldChild.pP = oldChild.props; oldChild.props = child.props; oldChild.f ||= child.f || node.f; if (typeof child.tag === "function") { oldChild[DOM_STASH][2] = child[DOM_STASH][2] || []; oldChild[DOM_STASH][3] = child[DOM_STASH][3]; if (!oldChild.f) { const prevPropsKeys = Object.keys(pP); const currentProps = oldChild.props; if (prevPropsKeys.length === Object.keys(currentProps).length && prevPropsKeys.every((k) => k in currentProps && currentProps[k] === pP[k])) { oldChild.s = true; } } } child = oldChild; } } else if (!isNodeString(child) && nameSpaceContext) { const ns = useContext(nameSpaceContext); if (ns) { child.n = ns; } } if (!isNodeString(child) && !child.s) { build(context, child); delete child.f; } vChildren.push(child); if (prevNode && !prevNode.s && !child.s) { for (let p = prevNode; p && !isNodeString(p); p = p.vC?.at(-1)) { p.nN = child; } } prevNode = child; } } node.vR = buildWithPreviousChildren ? [...node.vC, ...oldVChildren || []] : oldVChildren || []; node.vC = vChildren; if (buildWithPreviousChildren) { delete node.pC; } } catch (e) { node.f = true; if (e === cancelBuild) { if (foundErrorHandler) { return; } else { throw e; } } const [errorHandlerContext, errorHandler, errorHandlerNode] = node[DOM_STASH]?.[3] || []; if (errorHandler) { const fallbackUpdateFn = () => update([0, false, context[2]], errorHandlerNode); const fallbackUpdateFnArray = fallbackUpdateFnArrayMap.get(errorHandlerNode) || []; fallbackUpdateFnArray.push(fallbackUpdateFn); fallbackUpdateFnArrayMap.set(errorHandlerNode, fallbackUpdateFnArray); const fallback = errorHandler(e, () => { const fnArray = fallbackUpdateFnArrayMap.get(errorHandlerNode); if (fnArray) { const i = fnArray.indexOf(fallbackUpdateFn); if (i !== -1) { fnArray.splice(i, 1); return fallbackUpdateFn(); } } }); if (fallback) { if (context[0] === 1) { context[1] = true; } else { build(context, errorHandlerNode, [fallback]); if ((errorHandler.length === 1 || context !== errorHandlerContext) && errorHandlerNode.c) { apply(errorHandlerNode, errorHandlerNode.c, false); return; } } throw cancelBuild; } } throw e; } finally { if (foundErrorHandler) { context[5].pop(); } } }; var buildNode = (node) => { if (node === void 0 || node === null || typeof node === "boolean") { return void 0; } else if (typeof node === "string" || typeof node === "number") { return { t: node.toString(), d: true }; } else { if ("vR" in node) { node = { tag: node.tag, props: node.props, key: node.key, f: node.f, type: node.tag, ref: node.props.ref }; } if (typeof node.tag === "function") { ; node[DOM_STASH] = [0, []]; } else { const ns = nameSpaceMap[node.tag]; if (ns) { nameSpaceContext ||= createContext(""); node.props.children = [ { tag: nameSpaceContext, props: { value: node.n = `http://www.w3.org/${ns}`, children: node.props.children } } ]; } } return node; } }; var replaceContainer = (node, from, to) => { if (node.c === from) { node.c = to; node.vC.forEach((child) => replaceContainer(child, from, to)); } }; var updateSync = (context, node) => { node[DOM_STASH][2]?.forEach(([c, v]) => { c.values.push(v); }); try { build(context, node, void 0); } catch { return; } if (node.a) { delete node.a; return; } node[DOM_STASH][2]?.forEach(([c]) => { c.values.pop(); }); if (context[0] !== 1 || !context[1]) { apply(node, node.c, false); } }; var updateMap = /* @__PURE__ */ new WeakMap(); var currentUpdateSets = []; var update = async (context, node) => { context[5] ||= []; const existing = updateMap.get(node); if (existing) { existing[0](void 0); } let resolve; const promise = new Promise((r) => resolve = r); updateMap.set(node, [ resolve, () => { if (context[2]) { context[2](context, node, (context2) => { updateSync(context2, node); }).then(() => resolve(node)); } else { updateSync(context, node); resolve(node); } } ]); if (currentUpdateSets.length) { ; currentUpdateSets.at(-1).add(node); } else { await Promise.resolve(); const latest = updateMap.get(node); if (latest) { updateMap.delete(node); latest[1](); } } return promise; }; var renderNode = (node, container) => { const context = []; context[5] = []; context[4] = true; build(context, node, void 0); context[4] = false; const fragment = document.createDocumentFragment(); apply(node, fragment, true); replaceContainer(node, fragment, container); container.replaceChildren(fragment); }; var render = (jsxNode, container) => { renderNode(buildNode({ tag: "", props: { children: jsxNode } }), container); }; var flushSync = (callback) => { const set = /* @__PURE__ */ new Set(); currentUpdateSets.push(set); callback(); set.forEach((node) => { const latest = updateMap.get(node); if (latest) { updateMap.delete(node); latest[1](); } }); currentUpdateSets.pop(); }; var createPortal = (children, container, key) => ({ tag: HONO_PORTAL_ELEMENT, props: { children }, key, e: container, p: 1 }); export { build, buildDataStack, buildNode, createPortal, flushSync, getNameSpaceContext, render, renderNode, update };