Constant SERVER_COMPONENT_JS
Source pub const SERVER_COMPONENT_JS: &str = "/**\n * @param {number} eventId\n * @param {object | URLSearchParams} payload\n * @param {Node} target\n * @param {AbortController | undefined} abortController\n */\nasync function update(eventId, payload, target, abortController) {\n if (this.abortController) {\n this.abortController.abort();\n }\n abortController = this.abortController =\n abortController ?? new AbortController();\n const signal = this.abortController.signal;\n if (signal.aborted) {\n return;\n }\n\n try {\n let state = undefined;\n if (\n target.firstChild &&\n target.firstChild instanceof HTMLScriptElement &&\n target.firstChild.getAttribute(\"type\") === \"application/json\"\n ) {\n state = target.firstChild.innerText;\n }\n\n /** @type {RequestInit} */\n const req =\n payload instanceof URLSearchParams\n ? {\n signal,\n method: \"PUT\",\n body: (() => {\n const formData = new FormData();\n formData.append(\n \"payload\",\n new Blob([payload.toString()], {\n type: \"application/x-www-form-urlencoded\",\n }),\n );\n formData.append(\"event_id\", eventId);\n if (state) {\n formData.append(\n \"state\",\n new Blob([state], { type: \"application/json\" }),\n );\n }\n return formData;\n })(),\n }\n : {\n signal,\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: `{\"eventId\":${eventId},\"payload\":${JSON.stringify(payload)}${\n state ? `,\"state\":${state}` : \"\"\n }}`,\n };\n const endpoint =\n target instanceof CabinBoundary\n ? `/__boundary/${target.getAttribute(\"name\")}`\n : location.href;\n const res = await fetch(endpoint, req);\n if (signal.aborted) {\n return;\n }\n\n const url = new URL(res.url);\n if (res.ok && res.redirected && url.pathname === \"/client_redirect\") {\n window.location = url.search.substring(1);\n return;\n }\n\n if (!target.parentNode) {\n return;\n }\n\n if (res.status !== 200) {\n throw new Error(`received unexpected status code: ${res.status}`);\n }\n\n const html = await res.text();\n\n console.time(\"patch\");\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n patchChildren(target, template.content, {});\n console.timeEnd(\"patch\");\n\n const rewriteUrl = res.headers.get(\"location\");\n if (rewriteUrl && `${location.pathname}${location.search}` !== rewriteUrl) {\n history.pushState(null, undefined, rewriteUrl);\n }\n\n const newTitle = res.headers.get(\"x-cabin-title\");\n if (newTitle) {\n document.title = newTitle;\n }\n\n // TODO: prevent endless event loops\n {\n const eventId = res.headers.get(\"x-cabin-event\");\n const payload = res.headers.get(\"x-cabin-payload\");\n if (eventId && payload) {\n target.dispatchEvent(\n new CustomEvent(\"cabinFire\", {\n detail: { eventId, payload: JSON.parse(payload) },\n bubbles: true,\n }),\n );\n }\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") {\n // ignore\n } else {\n throw err;\n }\n } finally {\n if (this.abortController === abortController) {\n this.abortController = undefined;\n }\n }\n}\n\nfunction setUpEventListener(el, eventName, opts) {\n const attrName = `cabin-${eventName}`;\n /** @type {WeakMap<Element, AbortController>} */\n const abortControllers = new WeakMap();\n\n /**\n * @this {HTMLElement}\n * @param {Event} e\n */\n async function handleEvent(e) {\n /** @type {Element} */\n let node = e.target;\n\n do {\n const eventId = e.detail?.eventId ?? node.getAttribute(attrName);\n if (!eventId) {\n continue;\n }\n\n // The boundary only intercepts certain events\n if (opts.events && !opts.events.has(eventId)) {\n return;\n }\n\n // The internal state/view of the boundary is possibly going to change due to this event. To\n // force an update for the boundary if its parent view changes, remove all hash attributes\n // from ascendents up until the next cabin boundary.\n if (el !== document) {\n let el = this;\n do {\n el.removeAttribute(\"hash\");\n } while ((el = el.parentElement) && !(el instanceof CabinBoundary));\n }\n\n if (opts.disable && node.disabled) {\n return;\n }\n\n {\n const abortController = abortControllers.get(node);\n if (abortController) {\n abortController.abort();\n }\n }\n\n const abortController = new AbortController();\n abortControllers.set(node, abortController);\n\n const isSubmitEvent = eventName === \"submit\";\n e.stopPropagation();\n if (opts.preventDefault || isSubmitEvent) {\n e.preventDefault();\n }\n\n if (opts.debounce) {\n await new Promise((resolve) => setTimeout(resolve, opts.debounce));\n if (abortController.signal.aborted) {\n return;\n }\n }\n\n /** @type {WeakMap<HTMLElement, bool>} */\n const disabledBefore = new WeakMap();\n\n try {\n let payload;\n if (isSubmitEvent) {\n payload = new URLSearchParams(new FormData(node));\n } else if (opts?.eventPayload) {\n payload = JSON.parse(\n Object.entries(opts.eventPayload(e)).reduce(\n (result, [placeholder, value]) =>\n result.replace(placeholder, value),\n node.getAttribute(`${attrName}-payload`),\n ),\n );\n } else {\n payload =\n e.detail && typeof e.detail === \"object\" && \"payload\" in e.detail\n ? e.detail.payload\n : JSON.parse(node.getAttribute(`${attrName}-payload`));\n }\n\n if (isSubmitEvent) {\n // disable whole form\n for (const el of node.elements) {\n disabledBefore.set(el, el.disabled);\n el.disabled = true;\n }\n } else if (opts.disable) {\n node.disabled = true;\n }\n\n // Check for, and if exists apply, pre-rendered instances of this boundary\n if (!isSubmitEvent && this instanceof CabinBoundary) {\n let templates = [];\n let template = this.lastElementChild;\n while (\n template &&\n template instanceof HTMLTemplateElement &&\n template.hasAttribute(\"event-id\") &&\n template.hasAttribute(\"event-payload\")\n ) {\n templates.push(template);\n template = template.previousElementSibling;\n }\n for (const template of templates) {\n if (\n template.getAttribute(\"event-id\") === eventId &&\n template.getAttribute(\"event-payload\") === payload\n ) {\n console.time(\"patch\");\n patchChildren(el, template.content, {});\n // put back prerendered templates\n for (const template of templates) {\n this.appendChild(template);\n }\n console.timeEnd(\"patch\");\n return;\n }\n }\n }\n\n await update(\n parseInt(eventId),\n payload,\n el == document ? document.body : el,\n abortController,\n );\n } catch (err) {\n throw err;\n } finally {\n if (isSubmitEvent) {\n // restore disabled state\n for (const el of node.elements) {\n const before = disabledBefore.get(el);\n if (before !== undefined) {\n el.disabled = before;\n }\n }\n } else if (opts.disable) {\n node.disabled = false;\n }\n }\n\n break;\n } while ((node = node.parentElement));\n }\n\n el.addEventListener(eventName, function (e) {\n handleEvent.call(this, e).catch((err) => {\n console.error(err);\n });\n });\n}\n\n/**\n * @param {Node} rootBefore\n * @param {Node} rootAfter\n * @param {Record<string, Node>} orphanKeyed\n */\nfunction patchChildren(rootBefore, rootAfter, orphanKeyed) {\n // console.log(\"apply\", rootBefore, rootAfter);\n\n let nodeBefore = rootBefore.firstChild;\n let nodeAfter = rootAfter.firstChild;\n\n // Neither node has children, done here\n if (!nodeBefore && !nodeAfter) {\n return;\n }\n\n /** @type {Node | null} */\n let nextBefore = null;\n /** @type {Node | null} */\n let nextAfter = null;\n\n do {\n nextBefore = null;\n nextAfter = null;\n // console.log(nodeBefore, \"vs\", nodeAfter);\n\n if (!nodeAfter) {\n // console.log(\"removed\", nodeBefore);\n nextBefore = nodeBefore.nextSibling;\n rootBefore.removeChild(nodeBefore);\n continue;\n }\n\n // This node and all its next siblings are new nodes and can be directly added\n if (nodeBefore === null) {\n // console.log(\"append new\", nodeAfter, \"and siblings\");\n const fragment = document.createDocumentFragment();\n let node = nodeAfter;\n while (node) {\n let next = node.nextSibling;\n if (isKeyedElement(node)) {\n // Only checking orphan nodes as there are no siblings remaining to check anyway\n const previous = orphanKeyed[node.id];\n if (previous) {\n // console.log(`found existing ${node.id} and moved it into place`);\n fragment.appendChild(previous);\n delete orphanKeyed[node.id];\n node = next;\n continue;\n }\n }\n fragment.appendChild(node);\n node = next;\n }\n\n rootBefore.appendChild(fragment);\n return;\n }\n\n // re-use if found somewhere else in the tree\n if (\n isKeyedElement(nodeAfter) &&\n (!isKeyedElement(nodeBefore) || nodeBefore.id !== nodeAfter.id)\n ) {\n const previous =\n document.getElementById(nodeAfter.id) ?? orphanKeyed[nodeAfter.id];\n nextBefore = nodeBefore;\n nextAfter = nodeAfter.nextSibling;\n if (\n previous &&\n (!previous.parentNode || previous.parentNode == nodeBefore.parentNode)\n ) {\n // console.log(`found existing ${nodeAfter.id} and moved it into place`);\n rootBefore.insertBefore(previous, nodeBefore);\n delete orphanKeyed[nodeAfter.id];\n nodeBefore = previous;\n } else {\n // console.log(\"new iter item, move new into place\");\n rootBefore.insertBefore(nodeAfter, nodeBefore);\n continue;\n }\n }\n\n // type changed, replace completely\n else if (\n nodeBefore.nodeType !== nodeAfter.nodeType ||\n nodeBefore.nodeName !== nodeAfter.nodeName\n ) {\n // console.log(\"replace due to type change\");\n nextBefore = nodeBefore.nextSibling;\n nextAfter = nodeAfter.nextSibling;\n rootBefore.replaceChild(nodeAfter, nodeBefore);\n\n // Keep it around in case it got moved\n if (isKeyedElement(nodeBefore)) {\n orphanKeyed[nodeBefore.id] = nodeBefore;\n }\n\n continue;\n }\n\n switch (nodeAfter.nodeType) {\n case Node.COMMENT_NODE:\n throw new Error(\"unexpected comment\");\n\n case Node.ELEMENT_NODE:\n // skip sub-tree if hash is unchanged\n if (\n nodeBefore.hasAttribute(\"hash\") &&\n nodeAfter.hasAttribute(\"hash\") &&\n nodeBefore.getAttribute(\"hash\") == nodeAfter.getAttribute(\"hash\")\n ) {\n // console.log(\"skip due to unchanged hash\");\n break;\n }\n\n // console.log(\"patch attributes\");\n patchAttributes(nodeBefore, nodeAfter);\n patchChildren(nodeBefore, nodeAfter, orphanKeyed);\n break;\n\n case Node.TEXT_NODE:\n if (nodeAfter.textContent !== nodeBefore.textContent) {\n // console.log(\"update text\");\n nodeBefore.textContent = nodeAfter.textContent;\n } else {\n // console.log(\"text is unchanged\");\n }\n break;\n }\n } while (\n (nextAfter !== null || nextBefore !== null\n ? ((nodeAfter = nextAfter), (nodeBefore = nextBefore))\n : ((nodeAfter = nodeAfter?.nextSibling),\n (nodeBefore = nodeBefore?.nextSibling)),\n nodeAfter || nodeBefore)\n );\n}\n\n/**\n * @param {Element} childBefore\n * @param {Element} childAfter\n */\nfunction patchAttributes(childBefore, childAfter) {\n // special handling for certain elements\n switch (childAfter.nodeName) {\n case \"DIALOG\":\n if (childAfter.hasAttribute(\"open\")) {\n childBefore.show();\n } else {\n childBefore.close();\n }\n childAfter.removeAttribute(\"open\");\n }\n\n const oldAttributeNames = new Set(childBefore.getAttributeNames());\n for (const name of childAfter.getAttributeNames()) {\n oldAttributeNames.delete(name);\n\n if (ignoreAttribute(childAfter, name)) {\n continue;\n }\n\n const newValue = childAfter.getAttribute(name);\n switch (name) {\n case \"value\":\n if (childBefore.value !== newValue) {\n // console.log(\"update attribute\", name);\n childBefore.value = newValue;\n }\n break;\n default:\n if (childBefore.getAttribute(name) !== newValue) {\n // console.log(\"update attribute\", name);\n childBefore.setAttribute(name, newValue);\n }\n break;\n }\n }\n\n // delete attributes that are not set anymore\n for (const name of oldAttributeNames) {\n if (ignoreAttribute(childBefore, name)) {\n continue;\n }\n // console.log(\"remove attribute\", name);\n childBefore.removeAttribute(name);\n }\n}\n\n/**\n * @param {Element} el\n * @param {string} attr\n */\nfunction ignoreAttribute(el, attr) {\n switch (el.nodeName) {\n case \"DIALOG\":\n return attr === \"open\";\n default:\n return false;\n }\n}\n\n/**\n * @param {Node} node\n * @return {boolean}\n */\nfunction isKeyedElement(node) {\n return node.nodeType === Node.ELEMENT_NODE && node.nodeName === \"CABIN-KEYED\";\n}\n\nfunction setupEventListeners(el) {\n let events =\n el instanceof CabinBoundary\n ? new Set(\n el\n .getAttribute(\"events\")\n .split(\",\")\n .filter((s) => s.length > 0),\n )\n : null;\n if (events && events.size === 0) {\n events = undefined;\n }\n\n setUpEventListener(el, \"click\", {\n events,\n preventDefault: true,\n disable: true,\n });\n setUpEventListener(el, \"input\", {\n events,\n debounce: 500,\n eventPayload: (e) => ({ \"_##InputValue\": e.target.value }),\n });\n setUpEventListener(el, \"change\", {\n events,\n eventPayload: (e) => ({ \"_##InputValue\": e.target.value }),\n });\n setUpEventListener(el, \"submit\", {\n events,\n preventDefault: true,\n });\n setUpEventListener(el, \"cabinFire\", {\n events,\n });\n}\n\nclass CabinBoundary extends HTMLElement {\n constructor() {\n super();\n\n setupEventListeners(this);\n }\n}\n\ncustomElements.define(\"cabin-boundary\", CabinBoundary);\n\nsetupEventListeners(document);\n\ndocument.addEventListener(\"cabinRefresh\", async function () {\n // Force update all boundary content\n for (let el of document.querySelectorAll(\"cabin-boundary\")) {\n do {\n el.removeAttribute(\"hash\");\n } while ((el = el.parentElement) && !(el instanceof CabinBoundary));\n }\n await update(0, {}, document.body);\n});\n\ndocument.addEventListener(\"cabinFire\", async function (e) {\n await update(e.detail?.eventId, e.detail?.payload, document.body);\n});\n\nwindow.addEventListener(\"popstate\", () => {\n document.dispatchEvent(new CustomEvent(\"cabinRefresh\"));\n});\n";