SERVER_COMPONENT_JS

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";