UNPKG

hono

Version:

Web framework built on Web Standards

335 lines (334 loc) 8.57 kB
// src/jsx/dom/intrinsic-element/components.ts import { useContext } from "../../context.js"; import { use, useCallback, useMemo, useState } from "../../hooks/index.js"; import { dataPrecedenceAttr, deDupeKeyMap, domRenderers } from "../../intrinsic-element/common.js"; import { FormContext, registerAction } from "../hooks/index.js"; import { createPortal, getNameSpaceContext } from "../render.js"; var clearCache = () => { blockingPromiseMap = /* @__PURE__ */ Object.create(null); createdElements = /* @__PURE__ */ Object.create(null); }; var composeRef = (ref, cb) => { return useMemo( () => (e) => { let refCleanup; if (ref) { if (typeof ref === "function") { refCleanup = ref(e) || (() => { ref(null); }); } else if (ref && "current" in ref) { ref.current = e; refCleanup = () => { ref.current = null; }; } } const cbCleanup = cb(e); return () => { cbCleanup?.(); refCleanup?.(); }; }, [ref] ); }; var blockingPromiseMap = /* @__PURE__ */ Object.create(null); var createdElements = /* @__PURE__ */ Object.create(null); var documentMetadataTag = (tag, props, preserveNodeType, supportSort, supportBlocking) => { if (props?.itemProp) { return { tag, props, type: tag, ref: props.ref }; } const head = document.head; let { onLoad, onError, precedence, blocking, ...restProps } = props; let element = null; let created = false; const deDupeKeys = deDupeKeyMap[tag]; let existingElements = void 0; if (deDupeKeys.length > 0) { const tags = head.querySelectorAll(tag); LOOP: for (const e of tags) { for (const key of deDupeKeyMap[tag]) { if (e.getAttribute(key) === props[key]) { element = e; break LOOP; } } } if (!element) { const cacheKey = deDupeKeys.reduce( (acc, key) => props[key] === void 0 ? acc : `${acc}-${key}-${props[key]}`, tag ); created = !createdElements[cacheKey]; element = createdElements[cacheKey] ||= (() => { const e = document.createElement(tag); for (const key of deDupeKeys) { if (props[key] !== void 0) { e.setAttribute(key, props[key]); } if (props.rel) { e.setAttribute("rel", props.rel); } } return e; })(); } } else { existingElements = head.querySelectorAll(tag); } precedence = supportSort ? precedence ?? "" : void 0; if (supportSort) { restProps[dataPrecedenceAttr] = precedence; } const insert = useCallback( (e) => { if (deDupeKeys.length > 0) { let found = false; for (const existingElement of head.querySelectorAll(tag)) { if (found && existingElement.getAttribute(dataPrecedenceAttr) !== precedence) { head.insertBefore(e, existingElement); return; } if (existingElement.getAttribute(dataPrecedenceAttr) === precedence) { found = true; } } head.appendChild(e); } else if (existingElements) { let found = false; for (const existingElement of existingElements) { if (existingElement === e) { found = true; break; } } if (!found) { head.insertBefore( e, head.contains(existingElements[0]) ? existingElements[0] : head.querySelector(tag) ); } existingElements = void 0; } }, [precedence] ); const ref = composeRef(props.ref, (e) => { const key = deDupeKeys[0]; if (preserveNodeType === 2) { e.innerHTML = ""; } if (created || existingElements) { insert(e); } if (!onError && !onLoad) { return; } let promise = blockingPromiseMap[e.getAttribute(key)] ||= new Promise( (resolve, reject) => { e.addEventListener("load", resolve); e.addEventListener("error", reject); } ); if (onLoad) { promise = promise.then(onLoad); } if (onError) { promise = promise.catch(onError); } promise.catch(() => { }); }); if (supportBlocking && blocking === "render") { const key = deDupeKeyMap[tag][0]; if (props[key]) { const value = props[key]; const promise = blockingPromiseMap[value] ||= new Promise((resolve, reject) => { insert(element); element.addEventListener("load", resolve); element.addEventListener("error", reject); }); use(promise); } } const jsxNode = { tag, type: tag, props: { ...restProps, ref }, ref }; jsxNode.p = preserveNodeType; if (element) { jsxNode.e = element; } return createPortal( jsxNode, head ); }; var title = (props) => { const nameSpaceContext = getNameSpaceContext(); const ns = nameSpaceContext && useContext(nameSpaceContext); if (ns?.endsWith("svg")) { return { tag: "title", props, type: "title", ref: props.ref }; } return documentMetadataTag("title", props, void 0, false, false); }; var script = (props) => { if (!props || ["src", "async"].some((k) => !props[k])) { return { tag: "script", props, type: "script", ref: props.ref }; } return documentMetadataTag("script", props, 1, false, true); }; var style = (props) => { if (!props || !["href", "precedence"].every((k) => k in props)) { return { tag: "style", props, type: "style", ref: props.ref }; } props["data-href"] = props.href; delete props.href; return documentMetadataTag("style", props, 2, true, true); }; var link = (props) => { if (!props || ["onLoad", "onError"].some((k) => k in props) || props.rel === "stylesheet" && (!("precedence" in props) || "disabled" in props)) { return { tag: "link", props, type: "link", ref: props.ref }; } return documentMetadataTag("link", props, 1, "precedence" in props, true); }; var meta = (props) => { return documentMetadataTag("meta", props, void 0, false, false); }; var customEventFormAction = Symbol(); var form = (props) => { const { action, ...restProps } = props; if (typeof action !== "function") { ; restProps.action = action; } const [state, setState] = useState([null, false]); const onSubmit = useCallback( async (ev) => { const currentAction = ev.isTrusted ? action : ev.detail[customEventFormAction]; if (typeof currentAction !== "function") { return; } ev.preventDefault(); const formData = new FormData(ev.target); setState([formData, true]); const actionRes = currentAction(formData); if (actionRes instanceof Promise) { registerAction(actionRes); await actionRes; } setState([null, true]); }, [] ); const ref = composeRef(props.ref, (el) => { el.addEventListener("submit", onSubmit); return () => { el.removeEventListener("submit", onSubmit); }; }); const [data, isDirty] = state; state[1] = false; return { tag: FormContext, props: { value: { pending: data !== null, data, method: data ? "post" : null, action: data ? action : null }, children: { tag: "form", props: { ...restProps, ref }, type: "form", ref } }, f: isDirty }; }; var formActionableElement = (tag, { formAction, ...props }) => { if (typeof formAction === "function") { const onClick = useCallback((ev) => { ev.preventDefault(); ev.currentTarget.form.dispatchEvent( new CustomEvent("submit", { detail: { [customEventFormAction]: formAction } }) ); }, []); props.ref = composeRef(props.ref, (el) => { el.addEventListener("click", onClick); return () => { el.removeEventListener("click", onClick); }; }); } return { tag, props, type: tag, ref: props.ref }; }; var input = (props) => formActionableElement("input", props); var button = (props) => formActionableElement("button", props); Object.assign(domRenderers, { title, script, style, link, meta, form, input, button }); export { button, clearCache, composeRef, form, input, link, meta, script, style, title };