Skip to main content

INTERACTION_CLIENT

Constant INTERACTION_CLIENT 

Source
const INTERACTION_CLIENT: &str = "window.pharosErrorMessage = (body, fallback = \'Request failed.\') => {\n  try {\n    const value = JSON.parse(body);\n    const report = value?.error?.details?.schema === \'pharos.validation.report.v1\'\n      ? value.error.details : value;\n    let validationMessage = \'\';\n    if (report?.schema === \'pharos.validation.report.v1\') {\n      const entries = [\n        ...(Array.isArray(report.field_errors) ? report.field_errors.slice(0, 8) : []),\n        ...(Array.isArray(report.form_errors) ? report.form_errors.slice(0, 8) : [])\n      ].slice(0, 8);\n      const messages = entries.map((entry) => {\n        if (!entry || typeof entry !== \'object\') return \'\';\n        const code = typeof entry.code === \'string\' ? entry.code : \'\';\n        const field = typeof entry.field_id === \'string\' ? entry.field_id : \'\';\n        const message = typeof entry.message === \'string\' ? entry.message : \'\';\n        return [code, field && message ? `${field}: ${message}` : field || message]\n          .filter(Boolean).join(\', \');\n      }).filter(Boolean);\n      validationMessage = messages.join(\'; \');\n    }\n    if (value && typeof value.error === \'object\' && value.error) {\n      const fields = [\'code\', \'message\', \'correlation_id\']\n        .map((key) => key === \'message\' ? validationMessage || value.error[key] : value.error[key])\n        .filter((item) => typeof item === \'string\' && item.trim());\n      if (fields.length) return `error: ${fields.join(\', \')}`;\n    }\n    if (validationMessage) return `error: ${validationMessage}`;\n  } catch (_) {}\n  return body || fallback;\n};\n\nconst busy = (form, state) => {\n  form.toggleAttribute(\"aria-busy\", state);\n  form.querySelectorAll(\"button, input[type=submit]\").forEach((control) => {\n    control.disabled = state;\n  });\n};\n\nconst submittedValues = (form, submitter) => {\n  const values = new FormData(form);\n  if (submitter?.name) values.set(submitter.name, submitter.value);\n  return values;\n};\n\n// Keep the native swap target and one source boundary, including updated fragment keys.\nconst editableBoundaryFor = (node) => {\n  if (!(node instanceof HTMLElement)) return null;\n  const parent = node.parentElement;\n  if (!parent?.matches(\'[data-editor-key][data-editor-native]:not([data-editor-chrome])\') || parent.childElementCount !== 1 || parent.firstElementChild !== node) return null;\n  if (parent.getAttribute(\'data-editor-native\') !== node.tagName.toLowerCase()) return null;\n  if ([...parent.childNodes].some(child => child.nodeType === Node.TEXT_NODE && child.textContent.trim())) return null;\n  return parent;\n};\n\nwindow.pharosSyncEditableBoundary = (current, replacement) => {\n  const before = editableBoundaryFor(current);\n  const after = editableBoundaryFor(replacement);\n  if (!before || !after || current.tagName !== replacement.tagName || current.id !== replacement.id || before.getAttribute(\'data-editor-app\') !== after.getAttribute(\'data-editor-app\')) return false;\n  for (const attribute of [...before.attributes]) before.removeAttribute(attribute.name);\n  for (const attribute of after.attributes) before.setAttribute(attribute.name, attribute.value);\n  return true;\n};\n\ndocument.addEventListener(\'htmx:beforeSwap\', (event) => {\n  const detail = event.detail;\n  const current = detail?.target;\n  if (!detail?.shouldSwap || !editableBoundaryFor(current) || typeof detail.serverResponse !== \'string\' || detail.serverResponse.length > 8 * 1024 * 1024) return;\n  const source = detail.requestConfig?.elt;\n  const swap = source?.closest?.(\'[hx-swap]\')?.getAttribute(\'hx-swap\') || window.htmx?.config?.defaultSwapStyle;\n  if (swap?.trim().split(/\\s+/)[0] !== \'outerHTML\') return;\n  const parsed = document.createElement(\'template\');\n  parsed.innerHTML = detail.serverResponse;\n  let candidates;\n  if (current.id) candidates = [parsed.content.getElementById(current.id)].filter(Boolean);\n  else {\n    const boundary = editableBoundaryFor(current);\n    candidates = [...parsed.content.querySelectorAll(\'[data-editor-key][data-editor-native]\')]\n      .filter(node => node.getAttribute(\'data-editor-key\') === boundary.getAttribute(\'data-editor-key\'))\n      .map(node => node.firstElementChild)\n      .filter(node => node?.tagName === current.tagName && node.getAttribute(\'data-task-id\') === current.getAttribute(\'data-task-id\') && node.getAttribute(\'data-editor-instance\') === current.getAttribute(\'data-editor-instance\'));\n  }\n  if (candidates.length !== 1 || !window.pharosSyncEditableBoundary(current, candidates[0])) return;\n  const replacement = candidates[0];\n  replacement.parentElement.replaceWith(replacement);\n  detail.serverResponse = parsed.innerHTML;\n}, true);\n\nconst initializeRegion = (region) => {\n  if (!(region instanceof HTMLElement)) return;\n  window.htmx?.process(region);\n  region.dispatchEvent(new CustomEvent(\"pharos:afterSwap\", {\n    bubbles: true,\n    detail: {target: region}\n  }));\n};\n\nconst replaceTarget = (html, targetId) => {\n  const parsed = document.createElement(\"template\");\n  parsed.innerHTML = html;\n  parsed.content.querySelectorAll(\"[data-pharos-oob][id]\").forEach((replacement) => {\n    const current = document.getElementById(replacement.id);\n    if (current) {\n      window.pharosSyncEditableBoundary(current, replacement);\n      current.replaceWith(document.importNode(replacement, true));\n      initializeRegion(document.getElementById(replacement.id));\n    }\n  });\n  const replacement = parsed.content.getElementById(targetId);\n  const current = document.getElementById(targetId);\n  if (!replacement || !current) throw new Error(\"The response did not contain the declared region.\");\n  window.pharosSyncEditableBoundary(current, replacement);\n  current.replaceWith(document.importNode(replacement, true));\n  initializeRegion(document.getElementById(targetId));\n  document.getElementById(targetId)?.focus({preventScroll: true});\n  const announcer = document.querySelector(\"[data-pharos-announcer]\");\n  if (announcer) announcer.textContent = \"Content updated.\";\n};\n\nconst navigate = async (url, targetId, push) => {\n  document.body.setAttribute(\"aria-busy\", \"true\");\n  try {\n    const response = await fetch(url, {\n      credentials: \"same-origin\",\n      headers: {\"HX-Request\": \"true\", \"HX-Target\": targetId}\n    });\n    const html = await response.text();\n    if (!response.ok) throw new Error(window.pharosErrorMessage(html, `Navigation failed (${response.status})`));\n    replaceTarget(html, targetId);\n    if (push) history.pushState({pharosTarget: targetId}, \"\", url);\n  } finally {\n    document.body.removeAttribute(\"aria-busy\");\n  }\n};\n\ndocument.addEventListener(\"click\", (event) => {\n  const closeButton = event.target.closest(\"button[data-pharos-close-target]\");\n  if (closeButton) {\n    const target = document.getElementById(closeButton.dataset.pharosCloseTarget);\n    if (target) target.setAttribute(\"hidden\", \"\");\n    return;\n  }\n  const link = event.target.closest(\"a[data-pharos-nav]\");\n  if (!link || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n  const url = new URL(link.href, location.href);\n  if (url.origin !== location.origin) return;\n  event.preventDefault();\n  navigate(url, link.dataset.pharosNav, true).catch(() => { location.assign(url); });\n});\n\ndocument.addEventListener(\"submit\", (event) => {\n  const form = event.target.closest(\"form[data-pharos-get]\");\n  if (!form || event.defaultPrevented) return;\n  event.preventDefault();\n  const url = new URL(form.action, location.href);\n  const values = submittedValues(form, event.submitter);\n  for (const [name, value] of values) {\n    if (typeof value === \"string\" && value !== \"\") url.searchParams.append(name, value);\n  }\n  navigate(url, form.dataset.pharosGet, true).catch(() => { location.assign(url); });\n});\n\nwindow.addEventListener(\"popstate\", (event) => {\n  const targetId = event.state?.pharosTarget || \"app-main\";\n  navigate(location.href, targetId, false).catch(() => { location.reload(); });\n});\n\ndocument.addEventListener(\"submit\", async (event) => {\n  const form = event.target.closest(\"form[data-pharos-action]\");\n  if (!form || event.defaultPrevented) return;\n  event.preventDefault();\n  const targetId = form.dataset.pharosTarget;\n  const target = document.getElementById(targetId);\n  if (!target) return form.submit();\n  const values = submittedValues(form, event.submitter);\n  busy(form, true);\n  try {\n    const multipart = form.enctype === \"multipart/form-data\";\n    const response = await fetch(form.dataset.pharosAction, {\n      method: form.method.toUpperCase(),\n      body: multipart ? values : new URLSearchParams(values),\n      credentials: \"same-origin\",\n      headers: multipart ? {\"HX-Request\": \"true\", \"HX-Target\": targetId} : {\"Content-Type\": \"application/x-www-form-urlencoded\", \"HX-Request\": \"true\", \"HX-Target\": targetId}\n    });\n    const hxRedirect = response.headers.get(\"HX-Redirect\");\n    if (hxRedirect) {\n      location.assign(hxRedirect);\n      return;\n    }\n    const html = await response.text();\n    if (!response.ok) throw new Error(window.pharosErrorMessage(html, `Request failed (${response.status})`));\n    replaceTarget(html, targetId);\n  } catch (error) {\n    let status = form.querySelector(\"[data-pharos-error]\");\n    if (!status) {\n      status = document.createElement(\"p\");\n      status.dataset.pharosError = \"\";\n      status.setAttribute(\"role\", \"alert\");\n      form.append(status);\n    }\n    status.textContent = error instanceof Error ? error.message : \"The request could not be completed.\";\n  } finally {\n    busy(form, false);\n  }\n});\n";