phlex-reactive 0.15.2 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +101 -2
- data/README.md +14 -4
- data/app/javascript/phlex/reactive/core.min.js +6 -6
- data/app/javascript/phlex/reactive/core.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/bindings.js +31 -12
- data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
- data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/form.js +54 -22
- data/app/javascript/phlex/reactive/features/form.min.js +2 -2
- data/app/javascript/phlex/reactive/features/form.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/persist.js +31 -2
- data/app/javascript/phlex/reactive/features/persist.min.js +2 -2
- data/app/javascript/phlex/reactive/features/persist.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +6 -6
- data/app/javascript/phlex/reactive/runtime.js +148 -9
- data/lib/phlex/reactive/component/helpers.rb +9 -5
- data/lib/phlex/reactive/component/identity.rb +34 -1
- data/lib/phlex/reactive/component/image_fallback_binding.rb +54 -0
- data/lib/phlex/reactive/component/media_binding.rb +22 -4
- data/lib/phlex/reactive/component/media_pattern.rb +79 -0
- data/lib/phlex/reactive/doctor.rb +20 -8
- data/lib/phlex/reactive/js.rb +16 -5
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +3 -1
|
@@ -17,6 +17,8 @@ const wired = new WeakMap()
|
|
|
17
17
|
// The turbo:before-visit events a warn_unsaved root already prompted for: one
|
|
18
18
|
// prompt per visit, however many dirty roots guard the page (issue #298).
|
|
19
19
|
const askedVisits = new WeakSet()
|
|
20
|
+
// window -> its guarded roots and the removal of its one listener pair.
|
|
21
|
+
const guards = new WeakMap()
|
|
20
22
|
|
|
21
23
|
export function connect(controller, core) {
|
|
22
24
|
const root = controller.element
|
|
@@ -55,7 +57,7 @@ export function connect(controller, core) {
|
|
|
55
57
|
// beforeunload covers a real browser unload; turbo:before-visit covers a
|
|
56
58
|
// Turbo in-app navigation (it does NOT fire on restoration visits — the
|
|
57
59
|
// documented gap). Registered on window only when the marker is present.
|
|
58
|
-
if (root.getAttribute?.("data-reactive-warn-unsaved") === "true") armUnsavedGuard(root,
|
|
60
|
+
if (root.getAttribute?.("data-reactive-warn-unsaved") === "true") armUnsavedGuard(root, offs)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// Clipboard-trigger availability gate (issue #228) — ONLY when this root
|
|
@@ -193,29 +195,59 @@ function dirtyCount(root) {
|
|
|
193
195
|
// Arm the navigate-away guard (warn_unsaved: true, issue #103). beforeunload
|
|
194
196
|
// blocks a real browser unload; turbo:before-visit blocks a Turbo in-app
|
|
195
197
|
// navigation (it does NOT fire on restoration visits — the documented gap).
|
|
196
|
-
// Both read the LIVE dirty count, so a clean form never blocks.
|
|
197
|
-
//
|
|
198
|
-
|
|
198
|
+
// Both read the LIVE dirty count, so a clean form never blocks.
|
|
199
|
+
//
|
|
200
|
+
// One listener pair per window serves every guarded root on it: a pair per
|
|
201
|
+
// root made connecting and disconnecting N guarded forms quadratic (the
|
|
202
|
+
// browser scans a target's listeners on each add and remove). The pair is
|
|
203
|
+
// added with the first guarded root and removed with the last, so a page
|
|
204
|
+
// without one carries no beforeunload listener. The root leaves the set
|
|
205
|
+
// through `offs`, which disconnect() runs.
|
|
206
|
+
function armUnsavedGuard(root, offs) {
|
|
199
207
|
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
const win = window
|
|
210
|
+
let guard = guards.get(win)
|
|
211
|
+
if (!guard) {
|
|
212
|
+
const roots = new Set()
|
|
213
|
+
const dirtyRoot = () => {
|
|
214
|
+
for (const guarded of roots) if (dirtyCount(guarded) > 0) return guarded
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
const beforeunload = (event) => {
|
|
218
|
+
if (!dirtyRoot()) return undefined
|
|
219
|
+
// The spec dance: preventDefault + a truthy returnValue triggers the native
|
|
220
|
+
// "leave site?" prompt. The string is legacy (modern browsers show their own
|
|
221
|
+
// copy) but must be non-empty/truthy to arm the dialog.
|
|
222
|
+
event.preventDefault()
|
|
223
|
+
event.returnValue = "You have unsaved changes."
|
|
224
|
+
return event.returnValue
|
|
225
|
+
}
|
|
226
|
+
// One visit asks once (issue #298), and only when a guarded root is dirty.
|
|
227
|
+
// askedVisits still matters when two copies of this module (a bundled one
|
|
228
|
+
// beside the pinned one) each hold a pair.
|
|
229
|
+
const beforeVisit = (event) => {
|
|
230
|
+
if (askedVisits.has(event) || !dirtyRoot()) return
|
|
231
|
+
askedVisits.add(event)
|
|
232
|
+
const ok = typeof win.confirm === "function" ? win.confirm("You have unsaved changes. Leave anyway?") : true
|
|
233
|
+
if (!ok) event.preventDefault?.()
|
|
234
|
+
}
|
|
235
|
+
win.addEventListener("beforeunload", beforeunload)
|
|
236
|
+
win.addEventListener("turbo:before-visit", beforeVisit)
|
|
237
|
+
guard = {
|
|
238
|
+
roots,
|
|
239
|
+
remove: () => {
|
|
240
|
+
win.removeEventListener?.("beforeunload", beforeunload)
|
|
241
|
+
win.removeEventListener?.("turbo:before-visit", beforeVisit)
|
|
242
|
+
guards.delete(win)
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
guards.set(win, guard)
|
|
246
|
+
}
|
|
247
|
+
guard.roots.add(root)
|
|
248
|
+
offs.push(() => {
|
|
249
|
+
guard.roots.delete(root)
|
|
250
|
+
if (guard.roots.size === 0) guard.remove()
|
|
219
251
|
})
|
|
220
252
|
}
|
|
221
253
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=new WeakMap,p=new WeakSet,f=new WeakMap;function k(e,r){let t=e.element,n=[],o=[],a=(s,l,v)=>{s.addEventListener?.(l,v),n.push(()=>s.removeEventListener?.(l,v))},u,c,i=()=>{if(!u&&(u=b(t,r))){let s=()=>y(e,r);if(a(t,"turbo:morph-element",s),o.push(s),s(),t.getAttribute?.("data-reactive-warn-unsaved")==="true")w(t,n)}if(!c&&(c=h(t,r))){let s=()=>A(t,r);a(t,"turbo:morph-element",s),o.push(s),s()}};d.set(e,[n,()=>{for(let s of o)s();i()}]),i()}function E(e){for(let r of d.get(e)?.[0]??[])r();d.delete(e)}function S(e){d.get(e)?.[1]()}function b(e,r){if((e.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let t=e.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let n of t)if(r.owns(n))return!0;return!1}function y(e,r){let t=e.element;if(typeof t?.querySelectorAll!=="function")return;let n=0,o=(a,u)=>{if(u)a.setAttribute("data-reactive-dirty","true"),n++;else a.removeAttribute("data-reactive-dirty")};if(t.querySelectorAll("input[name], select[name], textarea[name]").forEach((a)=>{if(!r.owns(a))return;if(a.type==="file")return;o(a,m(a))}),t.querySelectorAll("[data-reactive-editable][name]").forEach((a)=>{if(r.owns(a))o(a,r.text(a)!==r.served(a))}),n>0)t.setAttribute("data-reactive-dirty",String(n));else t.removeAttribute("data-reactive-dirty")}function m(e){if(e.type==="checkbox"||e.type==="radio")return e.checked!==e.defaultChecked;if(e.tag==="select"||e.options){let r=Array.from(e.options??[]),t=!e.multiple&&!(e.size>1),n=r.filter((o)=>o.defaultSelected).pop()??r.find((o)=>!o.closest?.("optgroup")?.disabled&&!o.disabled);return r.some((o)=>o.selected!==(t?o===n:o.defaultSelected))}return e.value!==e.defaultValue}function g(e){let r=e.getAttribute?.("data-reactive-dirty"),t=Number(r);return Number.isFinite(t)&&t>0?t:0}function w(e,r){if(typeof window>"u"||typeof window.addEventListener!=="function")return;let t=window,n=f.get(t);if(!n){let o=new Set,a=()=>{for(let i of o)if(g(i)>0)return i;return null},u=(i)=>{if(!a())return;return i.preventDefault(),i.returnValue="You have unsaved changes.",i.returnValue},c=(i)=>{if(p.has(i)||!a())return;if(p.add(i),!(typeof t.confirm==="function"?t.confirm("You have unsaved changes. Leave anyway?"):!0))i.preventDefault?.()};t.addEventListener("beforeunload",u),t.addEventListener("turbo:before-visit",c),n={roots:o,remove:()=>{t.removeEventListener?.("beforeunload",u),t.removeEventListener?.("turbo:before-visit",c),f.delete(t)}},f.set(t,n)}n.roots.add(e),r.push(()=>{if(n.roots.delete(e),n.roots.size===0)n.remove()})}function h(e,r){if(e.getAttribute?.("data-reactive-clipboard"))return!0;let t=e.querySelectorAll?.("[data-reactive-clipboard]")??[];for(let n of t)if(r.owns(n))return!0;return!1}function A(e,r){let t=typeof globalThis.navigator?.clipboard?.readText==="function";if(e.getAttribute?.("data-reactive-clipboard"))e.hidden=!t;for(let n of e.querySelectorAll?.("[data-reactive-clipboard]")??[])if(r.owns(n))n.hidden=!t}export{k as connect,E as disconnect,S as reseed,y as scan};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=2DE40E9CEBCE1CEE64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=form.min.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["features/form.js"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// phlex/reactive/features/form — what a form root sets up when it connects\n// and re-syncs after a morph, with no round trip: dirty tracking and the\n// navigate-away guard (issue #103), and the paste-trigger availability gate\n// (issue #228). One of the feature modules the core imports on demand (issue\n// #275); in the default client it is part of the one file.\n//\n// The two share a module because they share a shape, not a page: a marker\n// check at connect, one pass over the root's own controls, the same pass\n// again on turbo:morph-element. The gate alone is smaller than what a module\n// of its own costs the core.\n//\n// This module never imports the core: it reaches a controller through the\n// `core` handle (core.owns — is this control this root's own, issue #15).\n\n// controller -> [the removals of what connect() added, its reseed].\nconst wired = new WeakMap()\n// The turbo:before-visit events a warn_unsaved root already prompted for: one\n// prompt per visit, however many dirty roots guard the page (issue #298).\nconst askedVisits = new WeakSet()\n\nexport function connect(controller, core) {\n const root = controller.element\n const offs = []\n const seeds = []\n // Add a listener and keep its removal. Closures, not a record of named\n // handlers: the minifier renames a local, never a property (issue #310).\n const listen = (target, type, handler) => {\n target.addEventListener?.(type, handler)\n offs.push(() => target.removeEventListener?.(type, handler))\n }\n // Each pass below is wired once, the first time its gate reads true: here,\n // and again from reseed() — a stream can render the root's first dirty field\n // or paste trigger into it after connect (issue #356).\n let tracking\n let gating\n const wire = () => {\n // Dirty tracking (issue #103) — ONLY when this root opts in (track_dirty: or a\n // reactive_field(dirty:)), so a component that never uses it pays nothing (no\n // baseline scan, no morph listener on every broadcast). A plain (outerHTML)\n // replace re-connects the controller, so seed the baseline scan here — the root\n // reflects current-vs-default WITHOUT waiting for the first input. An in-place\n // morph / broadcast morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO listen for turbo:morph-element on the root to re-scan\n // after the morph writes fresh default* attributes (reactive:applied is NOT a\n // valid hook — it fires when streams are handed to Turbo, BEFORE the DOM\n // mutation). Both listeners are torn down in disconnect().\n if (!tracking && (tracking = dirtyTrackingEnabled(root, core))) {\n const rescan = () => scan(controller, core)\n listen(root, \"turbo:morph-element\", rescan)\n seeds.push(rescan)\n rescan()\n\n // warn_unsaved: arm a navigate-away guard gated on a LIVE dirty-count read\n // (never a cached snapshot — the count is re-derived from the DOM each time).\n // beforeunload covers a real browser unload; turbo:before-visit covers a\n // Turbo in-app navigation (it does NOT fire on restoration visits — the\n // documented gap). Registered on window only when the marker is present.\n if (root.getAttribute?.(\"data-reactive-warn-unsaved\") === \"true\") armUnsavedGuard(root, listen)\n }\n\n // Clipboard-trigger availability gate (issue #228) — ONLY when this root\n // owns a paste trigger (on_client marks one with data-reactive-clipboard).\n // The Async Clipboard API is absent in insecure contexts and some webviews;\n // a paste button that can never work must not show. The gate OWNS a marked\n // trigger's `hidden` flag: author the trigger `hidden` and this pass reveals\n // it where the API exists (a dead button never paints); turbo:morph-element\n // re-syncs because a morph rewrites the trigger back to its authored hidden\n // state. A trigger first INTRODUCED by a later morph of a root that already\n // tracks dirtiness stays ungated (hidden) until a full replace re-connects\n // the controller; one a stream renders into the root is gated then.\n if (!gating && (gating = clipboardGateEnabled(root, core))) {\n const sync = () => syncClipboardTriggers(root, core)\n listen(root, \"turbo:morph-element\", sync)\n seeds.push(sync)\n sync()\n }\n }\n wired.set(controller, [\n offs,\n () => {\n for (const run of seeds) run()\n wire()\n },\n ])\n wire()\n}\n\n// Remove the listeners on disconnect (Turbo morph/navigation) so a morph\n// re-scan or a navigate-away guard never runs against a detached root.\nexport function disconnect(controller) {\n for (const off of wired.get(controller)?.[0] ?? []) off()\n wired.delete(controller)\n}\n\n// Re-run the passes wired at connect, then wire any whose gate reads true only\n// now — for content that changed after connect() ran (the draft restore, a\n// stream rendered into the root, issue #356).\nexport function reseed(controller) {\n wired.get(controller)?.[1]()\n}\n\n// Whether this root opts into dirty tracking (issue #103): track_dirty: puts the\n// trackDirty descriptor on the ROOT's data-action; a per-field reactive_field(\n// dirty:) puts it on a descendant field. Either turns tracking on. A quick\n// attribute read + one scoped query, evaluated once per connect (a cold path).\nfunction dirtyTrackingEnabled(root, core) {\n if ((root.getAttribute?.(\"data-action\") ?? \"\").includes(\"reactive#trackDirty\")) return true\n const nodes = root.querySelectorAll?.('[data-action*=\"reactive#trackDirty\"]') ?? []\n for (const el of nodes) if (core.owns(el)) return true\n return false\n}\n\n// Dirty tracking (issue #103). Wired by reactive_field(dirty: true) /\n// reactive_root(track_dirty: true): an `input` on an owned field runs a FULL\n// re-scan of every field this root owns (the controller's trackDirty action\n// calls this). NO round trip, NO shipped state.\n//\n// Re-compute the dirty flag for EVERY field this root owns in one pass, then\n// reflect the total onto the root. Called on an owned field's input, on\n// connect (baseline seed), and after a turbo:morph-element re-render (fresh\n// default* attrs). dirty = current ≠ the DOM's own default:\n// checkbox/radio → checked !== defaultChecked\n// select → some option.selected !== its reset state (issue #297)\n// else → value !== defaultValue\n// A full pass (not per-target) is REQUIRED: a radio group's previously-checked\n// radio flips to checked=false with NO input event, so per-target toggling\n// would leave its flag stale. File inputs are skipped — a file has no server\n// default baseline. Per-dirty-field data-reactive-dirty=\"true\" (\"true\" STRING,\n// not a valueless boolean attr — mirrors the on() flag convention); the root\n// carries data-reactive-dirty=\"<count>\" and DROPS the attr at zero, so\n// `[data-reactive-dirty]` styles the whole form and `[data-reactive-dirty]`\n// on a field styles just the changed control — both pure CSS, zero JS.\nexport function scan(controller, core) {\n const root = controller.element\n // Runs at bootstrap (the connect baseline seed) as well as on input/morph, so\n // it must never throw — degrade to a no-op if the root can't be queried (a real\n // reactive root always can; this guards minimal/test element stubs).\n if (typeof root?.querySelectorAll !== \"function\") return\n\n let count = 0\n const mark = (field, dirty) => {\n if (dirty) {\n field.setAttribute(\"data-reactive-dirty\", \"true\")\n count++\n } else {\n field.removeAttribute(\"data-reactive-dirty\")\n }\n }\n root.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!core.owns(field)) return // skip a nested reactive root's fields (issue #15)\n if (field.type === \"file\") return // no server default baseline to diff against\n mark(field, fieldDirty(field))\n })\n // A reactive_editable (issue #351) diffs its text against the text of its\n // last server render, which the core keeps.\n root.querySelectorAll(\"[data-reactive-editable][name]\").forEach((field) => {\n if (core.owns(field)) mark(field, core.text(field) !== core.served(field))\n })\n\n if (count > 0) root.setAttribute(\"data-reactive-dirty\", String(count))\n else root.removeAttribute(\"data-reactive-dirty\")\n}\n\n// Whether a single owned control differs from its server-rendered default.\nfunction fieldDirty(field) {\n if (field.type === \"checkbox\" || field.type === \"radio\") {\n return field.checked !== field.defaultChecked\n }\n if (field.tag === \"select\" || field.options) {\n // Any option whose selected state diverges from the select's RESET state.\n // Guard for a stub/absent options list (degrade to clean). A one-row single\n // select resets to its last defaultSelected option or, with none, to its\n // first enabled one — the browser selects it while its defaultSelected stays\n // false, so a pristine form must not read as dirty (issue #297). A multiple\n // (or size > 1) select resets to exactly its defaultSelected options. An\n // option inside a disabled <optgroup> is disabled too (the browser skips it).\n const options = Array.from(field.options ?? [])\n const single = !field.multiple && !(field.size > 1)\n const reset = options.filter((o) => o.defaultSelected).pop() ?? options.find((o) => !o.closest?.(\"optgroup\")?.disabled && !o.disabled)\n return options.some((o) => o.selected !== (single ? o === reset : o.defaultSelected))\n }\n return field.value !== field.defaultValue\n}\n\n// The live dirty-field count, re-derived from the DOM (never a cached snapshot)\n// — the source of truth for the warn_unsaved guard's gate.\nfunction dirtyCount(root) {\n const raw = root.getAttribute?.(\"data-reactive-dirty\")\n const n = Number(raw)\n return Number.isFinite(n) && n > 0 ? n : 0\n}\n\n// Arm the navigate-away guard (warn_unsaved: true, issue #103). beforeunload\n// blocks a real browser unload; turbo:before-visit blocks a Turbo in-app\n// navigation (it does NOT fire on restoration visits — the documented gap).\n// Both read the LIVE dirty count, so a clean form never blocks. Added through\n// connect()'s `listen`, so disconnect() removes exactly them.\nfunction armUnsavedGuard(root, listen) {\n if (typeof window === \"undefined\" || typeof window.addEventListener !== \"function\") return\n\n listen(window, \"beforeunload\", (event) => {\n if (dirtyCount(root) === 0) return undefined\n // The spec dance: preventDefault + a truthy returnValue triggers the native\n // \"leave site?\" prompt. The string is legacy (modern browsers show their own\n // copy) but must be non-empty/truthy to arm the dialog.\n event.preventDefault()\n event.returnValue = \"You have unsaved changes.\"\n return event.returnValue\n })\n // Every warn_unsaved root arms its own turbo:before-visit handler, and one\n // visit is one event dispatched to all of them: the first DIRTY root asks,\n // and the rest see the event already asked (issue #298). A clean root never\n // claims the event, so any dirty root on the page can still veto the visit.\n listen(window, \"turbo:before-visit\", (event) => {\n if (dirtyCount(root) === 0 || askedVisits.has(event)) return\n askedVisits.add(event)\n const ok = typeof window.confirm === \"function\" ? window.confirm(\"You have unsaved changes. Leave anyway?\") : true\n if (!ok) event.preventDefault?.()\n })\n}\n\n// Whether this root owns a clipboard-marked paste trigger (issue #228). The\n// ROOT itself counts (a button-only component that mixes on_client(paste_into)\n// onto reactive_root), then one scoped query; a NESTED root's triggers are\n// its own controller's to gate (issue #15 ownership).\nfunction clipboardGateEnabled(root, core) {\n if (root.getAttribute?.(\"data-reactive-clipboard\")) return true\n const nodes = root.querySelectorAll?.(\"[data-reactive-clipboard]\") ?? []\n for (const el of nodes) if (core.owns(el)) return true\n return false\n}\n\n// Set every owned paste trigger's `hidden` from clipboard availability\n// (issue #228): available → revealed (the authored `hidden` was only the\n// no-dead-button first paint), missing → hidden (insecure context /\n// webview). The gate owns the flag on MARKED elements only — nothing else\n// is ever touched. A marked ROOT is gated too: when the component IS the\n// paste button, hiding the root is exactly \"the dead button never shows\".\nfunction syncClipboardTriggers(root, core) {\n const available = typeof globalThis.navigator?.clipboard?.readText === \"function\"\n if (root.getAttribute?.(\"data-reactive-clipboard\")) root.hidden = !available\n for (const el of root.querySelectorAll?.(\"[data-reactive-clipboard]\") ?? []) {\n if (core.owns(el)) el.hidden = !available\n }\n}\n"
|
|
5
|
+
"// phlex/reactive/features/form — what a form root sets up when it connects\n// and re-syncs after a morph, with no round trip: dirty tracking and the\n// navigate-away guard (issue #103), and the paste-trigger availability gate\n// (issue #228). One of the feature modules the core imports on demand (issue\n// #275); in the default client it is part of the one file.\n//\n// The two share a module because they share a shape, not a page: a marker\n// check at connect, one pass over the root's own controls, the same pass\n// again on turbo:morph-element. The gate alone is smaller than what a module\n// of its own costs the core.\n//\n// This module never imports the core: it reaches a controller through the\n// `core` handle (core.owns — is this control this root's own, issue #15).\n\n// controller -> [the removals of what connect() added, its reseed].\nconst wired = new WeakMap()\n// The turbo:before-visit events a warn_unsaved root already prompted for: one\n// prompt per visit, however many dirty roots guard the page (issue #298).\nconst askedVisits = new WeakSet()\n// window -> its guarded roots and the removal of its one listener pair.\nconst guards = new WeakMap()\n\nexport function connect(controller, core) {\n const root = controller.element\n const offs = []\n const seeds = []\n // Add a listener and keep its removal. Closures, not a record of named\n // handlers: the minifier renames a local, never a property (issue #310).\n const listen = (target, type, handler) => {\n target.addEventListener?.(type, handler)\n offs.push(() => target.removeEventListener?.(type, handler))\n }\n // Each pass below is wired once, the first time its gate reads true: here,\n // and again from reseed() — a stream can render the root's first dirty field\n // or paste trigger into it after connect (issue #356).\n let tracking\n let gating\n const wire = () => {\n // Dirty tracking (issue #103) — ONLY when this root opts in (track_dirty: or a\n // reactive_field(dirty:)), so a component that never uses it pays nothing (no\n // baseline scan, no morph listener on every broadcast). A plain (outerHTML)\n // replace re-connects the controller, so seed the baseline scan here — the root\n // reflects current-vs-default WITHOUT waiting for the first input. An in-place\n // morph / broadcast morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO listen for turbo:morph-element on the root to re-scan\n // after the morph writes fresh default* attributes (reactive:applied is NOT a\n // valid hook — it fires when streams are handed to Turbo, BEFORE the DOM\n // mutation). Both listeners are torn down in disconnect().\n if (!tracking && (tracking = dirtyTrackingEnabled(root, core))) {\n const rescan = () => scan(controller, core)\n listen(root, \"turbo:morph-element\", rescan)\n seeds.push(rescan)\n rescan()\n\n // warn_unsaved: arm a navigate-away guard gated on a LIVE dirty-count read\n // (never a cached snapshot — the count is re-derived from the DOM each time).\n // beforeunload covers a real browser unload; turbo:before-visit covers a\n // Turbo in-app navigation (it does NOT fire on restoration visits — the\n // documented gap). Registered on window only when the marker is present.\n if (root.getAttribute?.(\"data-reactive-warn-unsaved\") === \"true\") armUnsavedGuard(root, offs)\n }\n\n // Clipboard-trigger availability gate (issue #228) — ONLY when this root\n // owns a paste trigger (on_client marks one with data-reactive-clipboard).\n // The Async Clipboard API is absent in insecure contexts and some webviews;\n // a paste button that can never work must not show. The gate OWNS a marked\n // trigger's `hidden` flag: author the trigger `hidden` and this pass reveals\n // it where the API exists (a dead button never paints); turbo:morph-element\n // re-syncs because a morph rewrites the trigger back to its authored hidden\n // state. A trigger first INTRODUCED by a later morph of a root that already\n // tracks dirtiness stays ungated (hidden) until a full replace re-connects\n // the controller; one a stream renders into the root is gated then.\n if (!gating && (gating = clipboardGateEnabled(root, core))) {\n const sync = () => syncClipboardTriggers(root, core)\n listen(root, \"turbo:morph-element\", sync)\n seeds.push(sync)\n sync()\n }\n }\n wired.set(controller, [\n offs,\n () => {\n for (const run of seeds) run()\n wire()\n },\n ])\n wire()\n}\n\n// Remove the listeners on disconnect (Turbo morph/navigation) so a morph\n// re-scan or a navigate-away guard never runs against a detached root.\nexport function disconnect(controller) {\n for (const off of wired.get(controller)?.[0] ?? []) off()\n wired.delete(controller)\n}\n\n// Re-run the passes wired at connect, then wire any whose gate reads true only\n// now — for content that changed after connect() ran (the draft restore, a\n// stream rendered into the root, issue #356).\nexport function reseed(controller) {\n wired.get(controller)?.[1]()\n}\n\n// Whether this root opts into dirty tracking (issue #103): track_dirty: puts the\n// trackDirty descriptor on the ROOT's data-action; a per-field reactive_field(\n// dirty:) puts it on a descendant field. Either turns tracking on. A quick\n// attribute read + one scoped query, evaluated once per connect (a cold path).\nfunction dirtyTrackingEnabled(root, core) {\n if ((root.getAttribute?.(\"data-action\") ?? \"\").includes(\"reactive#trackDirty\")) return true\n const nodes = root.querySelectorAll?.('[data-action*=\"reactive#trackDirty\"]') ?? []\n for (const el of nodes) if (core.owns(el)) return true\n return false\n}\n\n// Dirty tracking (issue #103). Wired by reactive_field(dirty: true) /\n// reactive_root(track_dirty: true): an `input` on an owned field runs a FULL\n// re-scan of every field this root owns (the controller's trackDirty action\n// calls this). NO round trip, NO shipped state.\n//\n// Re-compute the dirty flag for EVERY field this root owns in one pass, then\n// reflect the total onto the root. Called on an owned field's input, on\n// connect (baseline seed), and after a turbo:morph-element re-render (fresh\n// default* attrs). dirty = current ≠ the DOM's own default:\n// checkbox/radio → checked !== defaultChecked\n// select → some option.selected !== its reset state (issue #297)\n// else → value !== defaultValue\n// A full pass (not per-target) is REQUIRED: a radio group's previously-checked\n// radio flips to checked=false with NO input event, so per-target toggling\n// would leave its flag stale. File inputs are skipped — a file has no server\n// default baseline. Per-dirty-field data-reactive-dirty=\"true\" (\"true\" STRING,\n// not a valueless boolean attr — mirrors the on() flag convention); the root\n// carries data-reactive-dirty=\"<count>\" and DROPS the attr at zero, so\n// `[data-reactive-dirty]` styles the whole form and `[data-reactive-dirty]`\n// on a field styles just the changed control — both pure CSS, zero JS.\nexport function scan(controller, core) {\n const root = controller.element\n // Runs at bootstrap (the connect baseline seed) as well as on input/morph, so\n // it must never throw — degrade to a no-op if the root can't be queried (a real\n // reactive root always can; this guards minimal/test element stubs).\n if (typeof root?.querySelectorAll !== \"function\") return\n\n let count = 0\n const mark = (field, dirty) => {\n if (dirty) {\n field.setAttribute(\"data-reactive-dirty\", \"true\")\n count++\n } else {\n field.removeAttribute(\"data-reactive-dirty\")\n }\n }\n root.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!core.owns(field)) return // skip a nested reactive root's fields (issue #15)\n if (field.type === \"file\") return // no server default baseline to diff against\n mark(field, fieldDirty(field))\n })\n // A reactive_editable (issue #351) diffs its text against the text of its\n // last server render, which the core keeps.\n root.querySelectorAll(\"[data-reactive-editable][name]\").forEach((field) => {\n if (core.owns(field)) mark(field, core.text(field) !== core.served(field))\n })\n\n if (count > 0) root.setAttribute(\"data-reactive-dirty\", String(count))\n else root.removeAttribute(\"data-reactive-dirty\")\n}\n\n// Whether a single owned control differs from its server-rendered default.\nfunction fieldDirty(field) {\n if (field.type === \"checkbox\" || field.type === \"radio\") {\n return field.checked !== field.defaultChecked\n }\n if (field.tag === \"select\" || field.options) {\n // Any option whose selected state diverges from the select's RESET state.\n // Guard for a stub/absent options list (degrade to clean). A one-row single\n // select resets to its last defaultSelected option or, with none, to its\n // first enabled one — the browser selects it while its defaultSelected stays\n // false, so a pristine form must not read as dirty (issue #297). A multiple\n // (or size > 1) select resets to exactly its defaultSelected options. An\n // option inside a disabled <optgroup> is disabled too (the browser skips it).\n const options = Array.from(field.options ?? [])\n const single = !field.multiple && !(field.size > 1)\n const reset = options.filter((o) => o.defaultSelected).pop() ?? options.find((o) => !o.closest?.(\"optgroup\")?.disabled && !o.disabled)\n return options.some((o) => o.selected !== (single ? o === reset : o.defaultSelected))\n }\n return field.value !== field.defaultValue\n}\n\n// The live dirty-field count, re-derived from the DOM (never a cached snapshot)\n// — the source of truth for the warn_unsaved guard's gate.\nfunction dirtyCount(root) {\n const raw = root.getAttribute?.(\"data-reactive-dirty\")\n const n = Number(raw)\n return Number.isFinite(n) && n > 0 ? n : 0\n}\n\n// Arm the navigate-away guard (warn_unsaved: true, issue #103). beforeunload\n// blocks a real browser unload; turbo:before-visit blocks a Turbo in-app\n// navigation (it does NOT fire on restoration visits — the documented gap).\n// Both read the LIVE dirty count, so a clean form never blocks.\n//\n// One listener pair per window serves every guarded root on it: a pair per\n// root made connecting and disconnecting N guarded forms quadratic (the\n// browser scans a target's listeners on each add and remove). The pair is\n// added with the first guarded root and removed with the last, so a page\n// without one carries no beforeunload listener. The root leaves the set\n// through `offs`, which disconnect() runs.\nfunction armUnsavedGuard(root, offs) {\n if (typeof window === \"undefined\" || typeof window.addEventListener !== \"function\") return\n\n const win = window\n let guard = guards.get(win)\n if (!guard) {\n const roots = new Set()\n const dirtyRoot = () => {\n for (const guarded of roots) if (dirtyCount(guarded) > 0) return guarded\n return null\n }\n const beforeunload = (event) => {\n if (!dirtyRoot()) return undefined\n // The spec dance: preventDefault + a truthy returnValue triggers the native\n // \"leave site?\" prompt. The string is legacy (modern browsers show their own\n // copy) but must be non-empty/truthy to arm the dialog.\n event.preventDefault()\n event.returnValue = \"You have unsaved changes.\"\n return event.returnValue\n }\n // One visit asks once (issue #298), and only when a guarded root is dirty.\n // askedVisits still matters when two copies of this module (a bundled one\n // beside the pinned one) each hold a pair.\n const beforeVisit = (event) => {\n if (askedVisits.has(event) || !dirtyRoot()) return\n askedVisits.add(event)\n const ok = typeof win.confirm === \"function\" ? win.confirm(\"You have unsaved changes. Leave anyway?\") : true\n if (!ok) event.preventDefault?.()\n }\n win.addEventListener(\"beforeunload\", beforeunload)\n win.addEventListener(\"turbo:before-visit\", beforeVisit)\n guard = {\n roots,\n remove: () => {\n win.removeEventListener?.(\"beforeunload\", beforeunload)\n win.removeEventListener?.(\"turbo:before-visit\", beforeVisit)\n guards.delete(win)\n },\n }\n guards.set(win, guard)\n }\n guard.roots.add(root)\n offs.push(() => {\n guard.roots.delete(root)\n if (guard.roots.size === 0) guard.remove()\n })\n}\n\n// Whether this root owns a clipboard-marked paste trigger (issue #228). The\n// ROOT itself counts (a button-only component that mixes on_client(paste_into)\n// onto reactive_root), then one scoped query; a NESTED root's triggers are\n// its own controller's to gate (issue #15 ownership).\nfunction clipboardGateEnabled(root, core) {\n if (root.getAttribute?.(\"data-reactive-clipboard\")) return true\n const nodes = root.querySelectorAll?.(\"[data-reactive-clipboard]\") ?? []\n for (const el of nodes) if (core.owns(el)) return true\n return false\n}\n\n// Set every owned paste trigger's `hidden` from clipboard availability\n// (issue #228): available → revealed (the authored `hidden` was only the\n// no-dead-button first paint), missing → hidden (insecure context /\n// webview). The gate owns the flag on MARKED elements only — nothing else\n// is ever touched. A marked ROOT is gated too: when the component IS the\n// paste button, hiding the root is exactly \"the dead button never shows\".\nfunction syncClipboardTriggers(root, core) {\n const available = typeof globalThis.navigator?.clipboard?.readText === \"function\"\n if (root.getAttribute?.(\"data-reactive-clipboard\")) root.hidden = !available\n for (const el of root.querySelectorAll?.(\"[data-reactive-clipboard]\") ?? []) {\n if (core.owns(el)) el.hidden = !available\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "AAeA,IAAM,EAAQ,IAAI,QAGZ,EAAc,IAAI,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2DE40E9CEBCE1CEE64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -508,6 +508,35 @@ function persistClearRoot(root) {
|
|
|
508
508
|
// record's fields: the minifier renames a local, never a property (#310).
|
|
509
509
|
const wired = new WeakMap()
|
|
510
510
|
|
|
511
|
+
// document -> the submit-end handlers of its persisted roots. One document
|
|
512
|
+
// listener serves them all: a listener per root made connecting and
|
|
513
|
+
// disconnecting N persisted roots quadratic (the browser scans a target's
|
|
514
|
+
// listeners on each add and remove). Added with the first root, removed with
|
|
515
|
+
// the last. Returns the handler's removal.
|
|
516
|
+
const submitEnds = new WeakMap()
|
|
517
|
+
|
|
518
|
+
function onSubmitEndOf(doc, handler) {
|
|
519
|
+
if (typeof doc?.addEventListener !== "function") return () => {}
|
|
520
|
+
let shared = submitEnds.get(doc)
|
|
521
|
+
if (!shared) {
|
|
522
|
+
const handlers = new Set()
|
|
523
|
+
const listener = (event) => {
|
|
524
|
+
for (const run of handlers) run(event)
|
|
525
|
+
}
|
|
526
|
+
doc.addEventListener("turbo:submit-end", listener)
|
|
527
|
+
shared = [handlers, listener]
|
|
528
|
+
submitEnds.set(doc, shared)
|
|
529
|
+
}
|
|
530
|
+
const [handlers, listener] = shared
|
|
531
|
+
handlers.add(handler)
|
|
532
|
+
return () => {
|
|
533
|
+
handlers.delete(handler)
|
|
534
|
+
if (handlers.size > 0) return
|
|
535
|
+
doc.removeEventListener?.("turbo:submit-end", listener)
|
|
536
|
+
submitEnds.delete(doc)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
511
540
|
// Restore the draft into the owned controls, expose the state bag, announce,
|
|
512
541
|
// then arm the write listeners. The restore reads ONCE and never writes back:
|
|
513
542
|
// nothing is wired until it has completed, so no listener can clobber the
|
|
@@ -578,12 +607,12 @@ export function connect(controller, core, _morphed, pending) {
|
|
|
578
607
|
const inputs = ["input", ...PERSIST_EDITOR_CHANGE_EVENTS]
|
|
579
608
|
for (const event of inputs) root.addEventListener?.(event, onInput)
|
|
580
609
|
root.addEventListener?.("change", writeNow)
|
|
581
|
-
document
|
|
610
|
+
const offSubmitEnd = onSubmitEndOf(globalThis.document, onSubmitEnd)
|
|
582
611
|
wired.set(controller, () => {
|
|
583
612
|
if (timer !== null) writeNow()
|
|
584
613
|
for (const event of inputs) root.removeEventListener?.(event, onInput)
|
|
585
614
|
root.removeEventListener?.("change", writeNow)
|
|
586
|
-
|
|
615
|
+
offSubmitEnd()
|
|
587
616
|
})
|
|
588
617
|
|
|
589
618
|
// An edit made while we were on our way had no listener to draft it: draft
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var m;function
|
|
1
|
+
var m;function re(e){m=e}var L=1,V="phlex-reactive:persist:",G=new Set(["hidden","file","password","submit","button","reset","image"]),A=Symbol("persist-no-value"),k="data-reactive-persist-state",q=":is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])",H=new Set(["lexxy-editor","trix-editor"]),B="lexxy-editor, trix-editor, trix-toolbar",U=["lexxy:change","trix-change"],X=new Set(["","<p></p>","<p><br></p>","<div><br></div>"]),I=new WeakSet,P=new WeakSet,C=new WeakSet;function S(e){let i=e?.getAttribute?.("data-reactive-persist");if(!i||i==="off")return null;try{let r=JSON.parse(i);if(r&&typeof r==="object"&&typeof r.key==="string"&&r.key!=="")return r}catch{}if(!I.has(e))I.add(e),console.warn(`[phlex-reactive] malformed reactive_persist payload ${JSON.stringify(i)} — persistence disabled`);return null}function T(){try{return typeof localStorage>"u"?null:localStorage}catch{return null}}function N(e,i){if(e?.getAttribute?.("data-reactive-debug")!=="true"||P.has(e))return;P.add(e),console.info(`[phlex-reactive] reactive_persist: storage unavailable — draft skipped (${i?.name??i})`)}function R(e){return V+e.key}function b(e,i){let r=T();if(!r)return null;let t;try{t=r.getItem(R(i))}catch(u){return N(e,u),null}if(!t)return null;let n;try{n=JSON.parse(t)}catch{return null}if(!n||typeof n!=="object"||n.v!==L)return null;let s=Number(i.ttl)*1000;if(!(Number(n.savedAt)+s>Date.now()))return h(e,i),null;let c=n.fields&&typeof n.fields==="object"?n.fields:{},o=n.state&&typeof n.state==="object"?n.state:null;return{fields:c,state:o}}function O(e,i,{fields:r,state:t}){let n=T();if(!n)return!1;let s={v:L,savedAt:Date.now(),fields:r};if(t)s.state=t;try{return n.setItem(R(i),JSON.stringify(s)),!0}catch(c){return N(e,c),!1}}function h(e,i){e?.removeAttribute?.(k);let r=T();if(!r)return;try{r.removeItem(R(i))}catch(t){N(e,t)}}function v(e,i){let r=Array.isArray(i.fields)?new Set(i.fields):null,t=[],n=(s)=>s.closest('[data-controller~="reactive"]')===e&&s.getAttribute("data-reactive-persist")!=="off";for(let s of e.querySelectorAll("input[name], select[name], textarea[name]")){if(!n(s)||G.has(s.type)||s.closest(B))continue;if(r&&!r.has(s.name))continue;t.push({el:s,name:s.name,kind:"native"})}for(let s of e.querySelectorAll(q)){if(!n(s))continue;let c=Y(s);if(!c||r&&!r.has(c))continue;t.push({el:s,name:c,kind:H.has(s.localName)?"editor":"contenteditable"})}return t}function D(e){return e.hasAttribute("data-reactive-editable")?m.text(e):e.textContent??""}function Y(e){return e.getAttribute("name")||typeof e.name==="string"&&e.name||null}function g(e){return typeof e.value==="string"}function W(e){if(typeof e.isEmpty==="boolean")return e.isEmpty;let i=e.editor?.getDocument?.();if(typeof i?.isEmpty==="function")return i.isEmpty();return X.has(e.value.trim())}function K(e,i,r){if(e?.getAttribute?.("data-reactive-debug")!=="true"||C.has(e))return;C.add(e),console.info(`[phlex-reactive] reactive_persist: could not restore editor ${JSON.stringify(i)} — ${r?.message??r}`)}function y(e){return e.tagName==="SELECT"&&e.multiple}function j(e){let i=new Map;for(let{el:r,name:t}of e){if(r.type==="radio"||!String(t).endsWith("[]"))continue;i.set(t,(i.get(t)??0)+1)}return i}function M(e,i,r){if(!Array.isArray(e))return e;if(r.get(i)!==1||e.length!==1)return A;return e[0]}function _(e,i){let r={},t=(n)=>{let s=r[n];return Array.isArray(s)?s:r[n]=[]};for(let{el:n,name:s,kind:c}of v(e,i)){let o=String(s).endsWith("[]");if(c==="editor"){if(g(n))if(o)t(s).push(n.value);else r[s]=n.value}else if(c==="contenteditable"){let u=D(n);if(o)t(s).push(u);else r[s]=u}else if(n.type==="radio"){if(n.checked)r[s]=n.value;else if(!Object.hasOwn(r,s))r[s]=null}else if(n.type==="checkbox")if(o){let u=t(s);if(n.checked)u.push(n.value)}else r[s]=n.checked;else if(y(n)){let u=[...n.options].filter((f)=>f.selected).map((f)=>f.value);if(o)t(s).push(...u);else r[s]=u}else if(o)t(s).push(n.value);else r[s]=n.value}return r}function Q(e,i,r){let t=i.restore==="always",n=v(e,i),s=j(n),c=new Set;for(let o of n)if(o.kind==="native"&&o.el.type==="checkbox"&&o.el.checked)c.add(o.name);for(let{el:o,name:u,kind:f}of n){if(!Object.hasOwn(r,u))continue;let a=r[u];if(a===null||a===void 0)continue;if(Array.isArray(a)&&y(o)&&(s.get(u)??0)>1)continue;if(Array.isArray(a)&&!(o.type==="checkbox"||y(o))){if(a=M(a,u,s),a===A)continue}if((o.type==="checkbox"||y(o))&&String(u).endsWith("[]")&&!Array.isArray(a))continue;if(f==="editor")J(e,o,u,a,t);else if(f==="contenteditable"){if(!t&&D(o).trim()!=="")continue;o.textContent=String(a)}else if(o.type==="radio"){if(!t&&n.some((p)=>p.kind==="native"&&p.el.type==="radio"&&p.name===u&&p.el.checked))continue;o.checked=o.value===String(a)}else if(o.type==="checkbox"&&Array.isArray(a)){if(!t&&c.has(u))continue;o.checked=a.map(String).includes(o.value)}else if(o.type==="checkbox"){if(!t&&o.checked)continue;o.checked=Boolean(a)}else if(y(o)){if(!t&&[...o.options].some((d)=>d.selected))continue;let p=new Set((Array.isArray(a)?a:[a]).map(String));for(let d of o.options)d.selected=p.has(d.value)}else{if(!t&&o.value!=="")continue;o.value=String(a)}}Z(e,i,r)}function J(e,i,r,t,n){if(Array.isArray(t))return;if(!g(i))return;if(!n&&!W(i))return;try{i.value=String(t)}catch(s){K(e,r,s)}}function Z(e,i,r){let t=globalThis.customElements;if(typeof t?.whenDefined!=="function")return;let n=new Set;for(let c of e.querySelectorAll("lexxy-editor, trix-editor"))if(!g(c)&&!t.get?.(c.localName))n.add(c.localName);let s=i.restore==="always";for(let c of n)t.whenDefined(c).then(()=>{if(!e.isConnected)return;let o=v(e,i),u=j(o);for(let{el:f,name:a,kind:p}of o){if(p!=="editor"||f.localName!==c||!Object.hasOwn(r,a))continue;let d=M(r[a],a,u);if(d===null||d===void 0||d===A)continue;J(e,f,a,d,s)}})}function ee(e,i,r=!0){let t=S(e);if(!t){console.warn("[phlex-reactive] persist_state on a root without reactive_persist — skipped");return}if(!i||typeof i!=="object")return;let n=b(e,t),s={...n?.state??{},...i},c=r?_(e,t):n?.fields??{};if(O(e,t,{fields:c,state:s}))e.setAttribute?.(k,JSON.stringify(s))}function te(e){let i=S(e);if(i)h(e,i)}var w=new WeakMap,x=new WeakMap;function ne(e,i){if(typeof e?.addEventListener!=="function")return()=>{};let r=x.get(e);if(!r){let s=new Set,c=(o)=>{for(let u of s)u(o)};e.addEventListener("turbo:submit-end",c),r=[s,c],x.set(e,r)}let[t,n]=r;return t.add(i),()=>{if(t.delete(i),t.size>0)return;e.removeEventListener?.("turbo:submit-end",n),x.delete(e)}}function se(e,i,r,t){let n=e.element,s=S(n);if(!s)return;if(t?.submitted)h(n,s);let c=b(n,s);if(c){if(Q(n,s,c.fields),c.state)n.setAttribute?.(k,JSON.stringify(c.state));i.emit("reactive:persist-restored",{key:s.key,fields:c.fields,state:c.state??{}}),i.reseed()}let o=null,u=()=>{if(o!==null)clearTimeout(o),o=null},f=()=>{u();let l=b(n,s);O(n,s,{fields:_(n,s),state:l?.state??null})},a=()=>{let l=Number(s.debounce)||0;if(l<=0)return f();if(o!==null)clearTimeout(o);o=setTimeout(()=>{o=null,f()},l)},p=(l)=>{if(!l?.detail?.success)return;let E=l.target;if(E?.tagName!=="FORM"||typeof E.contains!=="function")return;if(!E.contains(n))return;u(),h(n,s)},d=["input",...U];for(let l of d)n.addEventListener?.(l,a);n.addEventListener?.("change",f);let z=ne(globalThis.document,p);if(w.set(e,()=>{if(o!==null)f();for(let l of d)n.removeEventListener?.(l,a);n.removeEventListener?.("change",f),z()}),t?.edited&&!t.submitted)f();for(let l of t?.ops??[])l(!0)}function oe(e,i){if(!i)return;let r=S(e);if(!r)return;if(i.submitted)return h(e,r);if(i.edited){let t=b(e,r),n={...t?.fields},s=_(e,r);for(let c of ie(e,r))if(Object.hasOwn(s,c))n[c]=s[c];O(e,r,{fields:n,state:t?.state??null})}for(let t of i.ops??[])t(!1)}function ie(e,i){let r=new Set;for(let{el:t,name:n,kind:s}of v(e,i))if(s==="editor"?g(t)&&!W(t):s==="contenteditable"?t.hasAttribute("data-reactive-editable")?m.text(t)!==m.served(t):(t.textContent??"").trim()!=="":t.type==="checkbox"||t.type==="radio"?t.checked!==t.defaultChecked:t.tagName==="SELECT"?[...t.options].some((o)=>o.selected!==o.hasAttribute("selected")):t.value!==t.defaultValue)r.add(n);return r}function ce(e){let i=w.get(e);w.delete(e),i?.()}function F(e,i){let r=m?.waiting.get(e)?.persist;if(!r||!S(e))return i(!0);(r.ops??=[]).push(i)}function ae(e,i){F(e,(r)=>ee(e,i,r))}function ue(e){F(e,()=>te(e))}export{q as PERSIST_EDITOR_SELECTOR,H as PERSIST_EDITOR_TAGS,oe as abandon,ue as clearRoot,se as connect,ce as disconnect,re as install,ae as writeState};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=2D40FF088B985FEC64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=persist.min.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["features/persist.js"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Client-only drafts (issue #239) — reactive_persist. A FEATURE MODULE of the\n// reactive controller (issue #275): the core imports it only when a root on\n// the page declares data-reactive-persist, or when a persist_state /\n// persist_clear client op runs.\n//\n// A root that declares\n// data-reactive-persist='{\"key\",\"ttl\",\"debounce\"[,\"fields\"][,\"restore\"]}'\n// keeps a localStorage draft of every persistable OWNED control (write on\n// input/change, restore on connect, clear on a successful submit / TTL / the\n// persist_clear op). The storage layer is MODULE-LEVEL and keyed on the root\n// element — the per-root connect/write path and the persist_state /\n// persist_clear client ops (which receive only the element) share it. Every\n// storage access is try/catch'd: a private window, a quota error or blocked\n// storage degrades to \"no draft\", never a thrown bootstrap. Nothing here\n// leaves the browser: no token, no POST, and phlex-reactive never writes\n// markup into the DOM: native controls are replayed via\n// .value/.checked/.selected, a bare [contenteditable] via textContent, and a\n// rich editor (lexxy-editor, trix-editor — issue #241) through its OWN\n// `value` setter, the same sanitizing import path a paste takes. Never\n// innerHTML.\n//\n// The feature contract (reactive_controller.js \"Feature modules\"):\n// install(shared) once, when the module loads\n// connect(controller, core, morphed, pending) per root connection\n// disconnect(controller, core) before the core's teardown\n// abandon(root, pending) the root left before connect\n// `core` is the controller's handle for features: core.emit(name, detail)\n// raw-dispatches a lifecycle event from the root; core.reseed() re-runs the\n// connect-time seeds that read field values (dirty baseline, show bindings,\n// on-complete arming, filter, tags, nested JSON, compute).\n//\n// TIMING. On the first root of a page that needs it, this module is still on\n// its way when the controller connects, so the restore runs a moment AFTER\n// connect() — and the connect-time seeds, which ran on the server's values,\n// are re-run (core.reseed()). On any later root the module is loaded and the\n// restore runs inside connect() as it always did.\n//\n// While a root waits for this module:\n// * it writes no draft, and its action requests are held back (they would\n// post the server's blanks) — the core's side of it;\n// * the core records, in `pending`, that the user edited a field and that\n// the form was submitted successfully; connect() drafts the edit, or\n// forgets the draft instead of restoring it;\n// * a persist_state / persist_clear op waits in pending.ops — its snapshot\n// would overwrite the draft with those blanks.\n// A root that leaves before the module arrives is handed to abandon().\n\n// The core's shared state (install()): shared.waiting maps a root to\n// { persist: pending } while that root waits for this module.\nlet shared\n\nexport function install(given) {\n shared = given\n}\n\nconst PERSIST_VERSION = 1\nconst PERSIST_PREFIX = \"phlex-reactive:persist:\"\n// Never persisted regardless of author intent: no server default to restore\n// into (hidden, file), secrets (password), and non-value controls.\nconst PERSIST_EXCLUDED_TYPES = new Set([\"hidden\", \"file\", \"password\", \"submit\", \"button\", \"reset\", \"image\"])\nconst PERSIST_NO_VALUE = Symbol(\"persist-no-value\")\nconst PERSIST_STATE_ATTR = \"data-reactive-persist-state\"\n// The editor query the core's #collectFields reads (minus the [name] guard —\n// an editor's name may live on its IDL `name` getter: Trix's `input=`-paired\n// hidden input). The core holds the same two constants (EDITOR_SELECTOR,\n// EDITOR_TAGS); a feature never imports the core, so they are repeated here\n// and spec/javascript/reactive_persist.test.js pins that they stay equal.\nexport const PERSIST_EDITOR_SELECTOR =\n \":is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])\"\nexport const PERSIST_EDITOR_TAGS = new Set([\"lexxy-editor\", \"trix-editor\"])\n// An editor's own chrome: Lexxy renders its toolbar (a `lexxy-code-language`\n// select, the link dialog's `href` input) INSIDE <lexxy-editor>, Trix as a\n// sibling <trix-toolbar>. Those are named native controls that are not the\n// user's fields — never drafted, never restored into.\nconst PERSIST_EDITOR_CHROME = \"lexxy-editor, trix-editor, trix-toolbar\"\n// The editors' own bubbling change events — the keystroke signal for the\n// draft write, since neither lets its contenteditable's native `input` bubble.\nconst PERSIST_EDITOR_CHANGE_EVENTS = [\"lexxy:change\", \"trix-change\"]\n// \"Is this editor empty\" when it exposes no predicate of its own (Lexxy's\n// `isEmpty`, Trix's `editor.getDocument().isEmpty()`): Lexxy's own empty\n// list plus the Trix / contenteditable empties. Exact strings, no HTML\n// parsing — an attachment-only body stays non-blank.\nconst PERSIST_EMPTY_HTML = new Set([\"\", \"<p></p>\", \"<p><br></p>\", \"<div><br></div>\"])\n// Once-per-root guards: a malformed payload warns once; the storage-failure\n// and editor-restore dev notes (debug mode only) print once each.\nconst persistPayloadWarned = new WeakSet()\nconst persistFailureNoted = new WeakSet()\nconst persistEditorNoted = new WeakSet()\n\n// The root's parsed payload, or null (undeclared / malformed → warned once).\n// A control-level \"off\" (reactive_persist_skip) is never a root payload.\nfunction persistPayload(root) {\n const raw = root?.getAttribute?.(\"data-reactive-persist\")\n if (!raw || raw === \"off\") return null\n try {\n const payload = JSON.parse(raw)\n if (payload && typeof payload === \"object\" && typeof payload.key === \"string\" && payload.key !== \"\") return payload\n } catch {\n // fall through to the warn\n }\n if (!persistPayloadWarned.has(root)) {\n persistPayloadWarned.add(root)\n console.warn(`[phlex-reactive] malformed reactive_persist payload ${JSON.stringify(raw)} — persistence disabled`)\n }\n return null\n}\n\nfunction persistStorage() {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage\n } catch {\n return null // the accessor itself can throw (blocked site data)\n }\n}\n\n// The dev lens for \"why did nothing come back\": ONLY under data-reactive-debug\n// (Phlex::Reactive.debug), once per root — production stays silent.\nfunction persistNoteFailure(root, error) {\n if (root?.getAttribute?.(\"data-reactive-debug\") !== \"true\" || persistFailureNoted.has(root)) return\n persistFailureNoted.add(root)\n console.info(`[phlex-reactive] reactive_persist: storage unavailable — draft skipped (${error?.name ?? error})`)\n}\n\nfunction persistKeyFor(payload) {\n return PERSIST_PREFIX + payload.key\n}\n\n// Read + validate the draft: null when absent, unparsable, another schema\n// version, or expired (an expired draft is REMOVED on read). Returns\n// { fields, state } with state null when the draft carries none.\nfunction persistRead(root, payload) {\n const store = persistStorage()\n if (!store) return null\n let raw\n try {\n raw = store.getItem(persistKeyFor(payload))\n } catch (error) {\n persistNoteFailure(root, error)\n return null\n }\n if (!raw) return null\n let draft\n try {\n draft = JSON.parse(raw)\n } catch {\n return null\n }\n if (!draft || typeof draft !== \"object\" || draft.v !== PERSIST_VERSION) return null\n const ttlMs = Number(payload.ttl) * 1000\n if (!(Number(draft.savedAt) + ttlMs > Date.now())) {\n persistRemove(root, payload)\n return null\n }\n const fields = draft.fields && typeof draft.fields === \"object\" ? draft.fields : {}\n const state = draft.state && typeof draft.state === \"object\" ? draft.state : null\n return { fields, state }\n}\n\nfunction persistWrite(root, payload, { fields, state }) {\n const store = persistStorage()\n if (!store) return false\n const draft = { v: PERSIST_VERSION, savedAt: Date.now(), fields }\n if (state) draft.state = state\n try {\n store.setItem(persistKeyFor(payload), JSON.stringify(draft))\n return true\n } catch (error) {\n persistNoteFailure(root, error)\n return false\n }\n}\n\nfunction persistRemove(root, payload) {\n root?.removeAttribute?.(PERSIST_STATE_ATTR)\n const store = persistStorage()\n if (!store) return\n try {\n store.removeItem(persistKeyFor(payload))\n } catch (error) {\n persistNoteFailure(root, error)\n }\n}\n\n// The persistable controls this root OWNS (#15: a nested reactive root's\n// controls are its own), minus the excluded types, the reactive_persist_skip\n// marker, and — when `fields` narrows the set — any undeclared name. Each\n// entry is { el, name, kind } with kind \"native\" (input/select/textarea),\n// \"editor\" (lexxy-editor / trix-editor) or \"contenteditable\" (a bare named\n// editable element) — issue #241.\nfunction persistControls(root, payload) {\n const allow = Array.isArray(payload.fields) ? new Set(payload.fields) : null\n const out = []\n const owned = (el) =>\n el.closest('[data-controller~=\"reactive\"]') === root && el.getAttribute(\"data-reactive-persist\") !== \"off\"\n for (const el of root.querySelectorAll(\"input[name], select[name], textarea[name]\")) {\n if (!owned(el) || PERSIST_EXCLUDED_TYPES.has(el.type) || el.closest(PERSIST_EDITOR_CHROME)) continue\n if (allow && !allow.has(el.name)) continue\n out.push({ el, name: el.name, kind: \"native\" })\n }\n for (const el of root.querySelectorAll(PERSIST_EDITOR_SELECTOR)) {\n if (!owned(el)) continue\n const name = persistEditorName(el)\n if (!name || (allow && !allow.has(name))) continue\n out.push({ el, name, kind: PERSIST_EDITOR_TAGS.has(el.localName) ? \"editor\" : \"contenteditable\" })\n }\n return out\n}\n\n// A contenteditable's text: a reactive_editable through the core's collector\n// (issue #351 — line breaks kept, as the request sends it), a bare one as its\n// textContent (#241).\nfunction persistText(el) {\n return el.hasAttribute(\"data-reactive-editable\") ? shared.text(el) : (el.textContent ?? \"\")\n}\n\n// The attribute first (Lexxy, a bare contenteditable — which has no `name`\n// IDL property at all), then the IDL getter (Trix resolves it through its\n// `input=`-paired hidden input, which is itself excluded as type=hidden).\nfunction persistEditorName(el) {\n return el.getAttribute(\"name\") || (typeof el.name === \"string\" && el.name) || null\n}\n\n// An editor is READY once its custom element has upgraded and connected: only\n// then does it expose the string `value` accessor (Lexxy's setter throws\n// before connectedCallback created its editor; Trix's discards the value).\nfunction persistEditorReady(el) {\n return typeof el.value === \"string\"\n}\n\n// Ask the editor whether it is empty (Lexxy `isEmpty`; Trix\n// `editor.getDocument().isEmpty()`), else the exact-string fallback. An\n// attachment-only server body is therefore NON-blank and never overwritten.\nfunction persistEditorBlank(el) {\n if (typeof el.isEmpty === \"boolean\") return el.isEmpty\n const doc = el.editor?.getDocument?.()\n if (typeof doc?.isEmpty === \"function\") return doc.isEmpty()\n return PERSIST_EMPTY_HTML.has(el.value.trim())\n}\n\n// The editor-restore dev lens: ONLY under data-reactive-debug, once per root.\nfunction persistNoteEditorFailure(root, name, error) {\n if (root?.getAttribute?.(\"data-reactive-debug\") !== \"true\" || persistEditorNoted.has(root)) return\n persistEditorNoted.add(root)\n console.info(\n `[phlex-reactive] reactive_persist: could not restore editor ${JSON.stringify(name)} — ${error?.message ?? error}`,\n )\n}\n\nfunction persistSelectMultiple(el) {\n return el.tagName === \"SELECT\" && el.multiple\n}\n\n// How many controls can contribute a value under each `[]` name, counted the\n// way persistSnapshot fills the slot — a radio is the exception there and here.\n// A group of ONE is unambiguous: its single entry can only have come from that\n// control.\nfunction persistGroupSizes(controls) {\n const sizes = new Map()\n for (const { el, name } of controls) {\n if (el.type === \"radio\" || !String(name).endsWith(\"[]\")) continue\n sizes.set(name, (sizes.get(name) ?? 0) + 1)\n }\n return sizes\n}\n\n// The value a control that cannot pick its own entry out of a list may take.\n// A list belongs to a `[]` group, and in a group of two or more nothing says\n// which entry came from which control — restoring it would paste\n// \"freeform,news\" into a text field. A group of ONE has no such ambiguity, and\n// refusing it would silently drop the draft of a plain field whose name merely\n// ends in `[]` (a list JS maintains), which worked before groups existed.\n// Returns PERSIST_NO_VALUE when the control must keep what the server rendered.\nfunction persistGenericValue(value, name, sizes) {\n if (!Array.isArray(value)) return value\n if (sizes.get(name) !== 1 || value.length !== 1) return PERSIST_NO_VALUE\n\n return value[0]\n}\n\n// Snapshot the owned controls: radio → the checked value (null when the group\n// has none, so a restore leaves it alone), checkbox → checked, multi-select →\n// the selected values, a rich editor → its serialized `value` (omitted while\n// the element hasn't upgraded — never a phantom \"\"), a bare contenteditable →\n// its textContent, else .value. Mirrors the core's #collectFields reads.\nfunction persistSnapshot(root, payload) {\n const fields = {}\n // A `[]` name is a group for every control that can contribute a value —\n // checkboxes, selects, text inputs, editors and contenteditables all append\n // to one array, mirroring #collectFields. The one exception is a RADIO group,\n // which means \"pick one\" and keeps its single value with or without the\n // suffix, exactly as the collector treats it.\n //\n // Reading the slot without this (fields[name] ?? []) breaks as soon as a\n // non-checkbox shares the group's name: a text input or an editor leaves a\n // string there, `.push` on it throws inside the draft write, and that write\n // is swallowed — the root then persists nothing at all, silently. Editors\n // are collected AFTER the native controls, so they always land last.\n const groupSlot = (name) => {\n const existing = fields[name]\n return Array.isArray(existing) ? existing : (fields[name] = [])\n }\n for (const { el, name, kind } of persistControls(root, payload)) {\n const group = String(name).endsWith(\"[]\")\n if (kind === \"editor\") {\n if (persistEditorReady(el)) {\n if (group) groupSlot(name).push(el.value)\n else fields[name] = el.value\n }\n } else if (kind === \"contenteditable\") {\n const text = persistText(el)\n if (group) groupSlot(name).push(text)\n else fields[name] = text\n } else if (el.type === \"radio\") {\n if (el.checked) fields[name] = el.value\n else if (!Object.hasOwn(fields, name)) fields[name] = null\n } else if (el.type === \"checkbox\") {\n // A `[]` group drafts the list of ticked values, mirroring #collectFields\n // (issue #258). Without this the boxes overwrote each other and the draft\n // held one boolean, which the restore then applied to every box of the\n // group. A lone checkbox keeps the boolean it has always been.\n if (group) {\n const slot = groupSlot(name)\n if (el.checked) slot.push(el.value)\n } else {\n fields[name] = el.checked\n }\n } else if (persistSelectMultiple(el)) {\n const selected = [...el.options].filter((o) => o.selected).map((o) => o.value)\n if (group) groupSlot(name).push(...selected)\n else fields[name] = selected\n } else if (group) {\n groupSlot(name).push(el.value)\n } else {\n fields[name] = el.value\n }\n }\n return fields\n}\n\n// Replay the draft into the owned controls. Default (restore: blank): a\n// control the server rendered NON-BLANK keeps its value — a 422 re-render's\n// submitted values beat an older draft. restore: \"always\" lets the draft win.\n// Values land via .value/.checked/.selected, textContent, or the editor's own\n// `value` setter — never HTML written by us.\nfunction persistApply(root, payload, fields) {\n const always = payload.restore === \"always\"\n const controls = persistControls(root, payload)\n // Which group names the SERVER rendered with a box already ticked. Computed\n // BEFORE the loop on purpose: the loop writes `checked`, so asking this\n // question from inside it would read THIS restore's own work — the first box\n // it ticks makes every later box of the same group look server-rendered, and\n // a draft of two values comes back as one. The radio branch below asks the\n // same question inline and stays correct only because a radio group holds a\n // single value.\n const groupSizes = persistGroupSizes(controls)\n const serverTicked = new Set()\n for (const control of controls) {\n if (control.kind === \"native\" && control.el.type === \"checkbox\" && control.el.checked) {\n serverTicked.add(control.name)\n }\n }\n for (const { el, name, kind } of controls) {\n if (!Object.hasOwn(fields, name)) continue\n let value = fields[name]\n if (value === null || value === undefined) continue\n // A multi-select reads a list by matching option values, which is only\n // sound when the list is ITS list. In a group with another contributor the\n // entries are mixed, and a text value that happens to equal an option\n // would select it — measured, a draft of [\"blue\",\"freitext\"] from a select\n // plus a text field selected both options. `?? 0` because a name without\n // the suffix is not in the map at all, and a plain `<select multiple\n // name=\"colors\">` must keep restoring.\n if (Array.isArray(value) && persistSelectMultiple(el) && (groupSizes.get(name) ?? 0) > 1) continue\n\n // An array belongs to a `[]` group, and only a control that can pick ITS\n // entry out of the list may read it: a checkbox matches by value, a\n // multi-select by option. Everything else — editors, contenteditables,\n // text inputs — keeps what the server rendered, because the list does not\n // record which entry came from which control. This sits ABOVE the branch\n // chain on purpose: below the editor branch it would never fire for the\n // very controls that land last in the snapshot.\n if (Array.isArray(value) && !(el.type === \"checkbox\" || persistSelectMultiple(el))) {\n value = persistGenericValue(value, name, groupSizes)\n if (value === PERSIST_NO_VALUE) continue\n }\n // The mirror, for a draft written BEFORE a group was drafted as a list\n // (#258): there `features[]` held ONE boolean, and applying it here ticks\n // every box of the group — precisely the state this fix removes, for as\n // long as the draft lives (default ttl 7 days). A group key that is not a\n // list is stale by definition, so the control keeps what the server\n // rendered and the next snapshot overwrites the key. It reads for a\n // multi-select too: 0.13.2 wrote last-writer-wins per name, so a checkbox\n // in a mixed group could leave its boolean under the select's name, and\n // under `restore: \"always\"` the select would then deselect everything —\n // `wanted` being Set{\"true\"} matches no option. Asking for the `[]` suffix\n // is what leaves a lone `gift` checkbox on the boolean it has always held —\n // and scoping the rule to the one key whose meaning changed is why\n // PERSIST_VERSION stays at 1: bumping it would also throw away the drafted\n // prose of every form that has no checkbox group at all.\n if ((el.type === \"checkbox\" || persistSelectMultiple(el)) && String(name).endsWith(\"[]\") && !Array.isArray(value)) {\n continue\n }\n if (kind === \"editor\") {\n persistApplyEditor(root, el, name, value, always)\n } else if (kind === \"contenteditable\") {\n if (!always && persistText(el).trim() !== \"\") continue\n el.textContent = String(value)\n } else if (el.type === \"radio\") {\n if (!always && controls.some((c) => c.kind === \"native\" && c.el.type === \"radio\" && c.name === name && c.el.checked)) continue\n el.checked = el.value === String(value)\n } else if (el.type === \"checkbox\" && Array.isArray(value)) {\n // A drafted group ticks exactly the boxes it held. One box the SERVER\n // rendered ticked means it had a say, and the draft yields for the whole\n // group.\n if (!always && serverTicked.has(name)) continue\n el.checked = value.map(String).includes(el.value)\n } else if (el.type === \"checkbox\") {\n if (!always && el.checked) continue\n el.checked = Boolean(value)\n } else if (persistSelectMultiple(el)) {\n if (!always && [...el.options].some((o) => o.selected)) continue\n const wanted = new Set((Array.isArray(value) ? value : [value]).map(String))\n for (const o of el.options) o.selected = wanted.has(o.value)\n } else {\n if (!always && el.value !== \"\") continue\n el.value = String(value)\n }\n }\n persistDeferEditors(root, payload, fields)\n}\n\n// A ready editor takes the value through its own setter; the setter is the\n// editor's sanitizing import (Trix HTMLParser, Lexxy $generateNodesFromDOM +\n// sanitizer). A throw (Lexxy before its editor exists) never escapes connect.\nfunction persistApplyEditor(root, el, name, value, always) {\n // A list never reaches here: both callers resolve it through\n // persistGenericValue first — the group of two or more has no mapping back to\n // this editor, the group of one does. Belt and braces, because this is the\n // one apply path with a second entry point.\n if (Array.isArray(value)) return\n if (!persistEditorReady(el)) return // not upgraded yet — persistDeferEditors re-applies after define\n if (!always && !persistEditorBlank(el)) return\n try {\n el.value = String(value)\n } catch (error) {\n persistNoteEditorFailure(root, name, error)\n }\n}\n\n// An editor whose custom element is not defined yet (Trix defines its elements\n// in a setTimeout after load; a lazily imported Lexxy) cannot take a value now.\n// Re-apply per TAG once it is defined — re-querying the controls (the upgrade\n// may replace the node) and re-checking restore: blank at that moment — unless\n// the root has left the document meanwhile.\nfunction persistDeferEditors(root, payload, fields) {\n const registry = globalThis.customElements\n if (typeof registry?.whenDefined !== \"function\") return\n const pending = new Set()\n for (const el of root.querySelectorAll(\"lexxy-editor, trix-editor\")) {\n if (!persistEditorReady(el) && !registry.get?.(el.localName)) pending.add(el.localName)\n }\n const always = payload.restore === \"always\"\n for (const tag of pending) {\n registry.whenDefined(tag).then(() => {\n if (!root.isConnected) return\n const deferred = persistControls(root, payload)\n const sizes = persistGroupSizes(deferred)\n for (const { el, name, kind } of deferred) {\n if (kind !== \"editor\" || el.localName !== tag || !Object.hasOwn(fields, name)) continue\n const value = persistGenericValue(fields[name], name, sizes)\n if (value === null || value === undefined || value === PERSIST_NO_VALUE) continue\n persistApplyEditor(root, el, name, value, always)\n }\n })\n }\n}\n\n// The persist_state op body: merge a FLAT bag into the root's draft (re-\n// snapshotting the fields so the write is whole) and mirror it on the root.\n// A root without reactive_persist is a call-site bug — warn and skip.\n// `restored` false: the root never restored (it left before this module\n// arrived), so its controls hold the server's blanks — keep the drafted\n// fields and merge the bag only.\nfunction persistWriteState(root, state, restored = true) {\n const payload = persistPayload(root)\n if (!payload) {\n console.warn(\"[phlex-reactive] persist_state on a root without reactive_persist — skipped\")\n return\n }\n if (!state || typeof state !== \"object\") return\n const current = persistRead(root, payload)\n const merged = { ...(current?.state ?? {}), ...state }\n const fields = restored ? persistSnapshot(root, payload) : (current?.fields ?? {})\n if (persistWrite(root, payload, { fields, state: merged })) {\n root.setAttribute?.(PERSIST_STATE_ATTR, JSON.stringify(merged))\n }\n}\n\nfunction persistClearRoot(root) {\n const payload = persistPayload(root)\n if (payload) persistRemove(root, payload)\n}\n\n// --- Per-root wiring -------------------------------------------------------\n\n// controller -> the closure that flushes and unwires the connection this\n// feature is wired to. Its timer and handlers are locals of connect(), not a\n// record's fields: the minifier renames a local, never a property (#310).\nconst wired = new WeakMap()\n\n// Restore the draft into the owned controls, expose the state bag, announce,\n// then arm the write listeners. The restore reads ONCE and never writes back:\n// nothing is wired until it has completed, so no listener can clobber the\n// draft with the server's blanks. The submit-end listener is DOCUMENT-level\n// (the event fires on the form, which is usually an ANCESTOR of this root)\n// and gated on the form containing this root.\n//\n// `pending` is what the core recorded while this root waited for the module\n// (undefined when it never had to): see TIMING at the top.\nexport function connect(controller, core, _morphed, pending) {\n const root = controller.element\n const payload = persistPayload(root)\n if (!payload) return\n // Submitted successfully while we were on our way: the draft is done with.\n if (pending?.submitted) persistRemove(root, payload)\n const draft = persistRead(root, payload)\n if (draft) {\n persistApply(root, payload, draft.fields)\n if (draft.state) root.setAttribute?.(PERSIST_STATE_ATTR, JSON.stringify(draft.state))\n core.emit(\"reactive:persist-restored\", { key: payload.key, fields: draft.fields, state: draft.state ?? {} })\n // If the connect-time seeds ran before this restore, they ran on the\n // server's values; they read fields.\n core.reseed()\n }\n\n // ONE pending keystroke write per root (a snapshot is a full pass, so\n // per-field timers would only multiply writes).\n let timer = null\n const cancel = () => {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n // Snapshot every persistable owned control and write. Re-reads the current\n // draft first so a state bag written by persist_state survives the write\n // (the bag lives in storage, not on the connection — the op has none).\n const writeNow = () => {\n cancel()\n const current = persistRead(root, payload)\n persistWrite(root, payload, { fields: persistSnapshot(root, payload), state: current?.state ?? null })\n }\n // Trailing-edge debounce for keystrokes.\n const onInput = () => {\n const ms = Number(payload.debounce) || 0\n if (ms <= 0) return writeNow()\n if (timer !== null) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n writeNow()\n }, ms)\n }\n // A SUCCESSFUL Turbo form submission of the form that owns this root\n // forgets the draft. tagName (not instanceof) so a cross-realm form counts.\n const onSubmitEnd = (event) => {\n if (!event?.detail?.success) return\n const form = event.target\n if (form?.tagName !== \"FORM\" || typeof form.contains !== \"function\") return\n if (!form.contains(root)) return\n // Drop a pending keystroke write too — the disconnect flush that follows\n // Turbo's redirect visit would otherwise resurrect the just-cleared draft.\n cancel()\n persistRemove(root, payload)\n }\n // Rich editors (#241): Lexical and Trix swallow the native `input` of\n // their contenteditable, so their own bubbling change events are the\n // keystroke signal — same trailing-edge debounce as `input`.\n const inputs = [\"input\", ...PERSIST_EDITOR_CHANGE_EVENTS]\n for (const event of inputs) root.addEventListener?.(event, onInput)\n root.addEventListener?.(\"change\", writeNow)\n document.addEventListener?.(\"turbo:submit-end\", onSubmitEnd)\n wired.set(controller, () => {\n if (timer !== null) writeNow()\n for (const event of inputs) root.removeEventListener?.(event, onInput)\n root.removeEventListener?.(\"change\", writeNow)\n document.removeEventListener?.(\"turbo:submit-end\", onSubmitEnd)\n })\n\n // An edit made while we were on our way had no listener to draft it: draft\n // it now, with everything the restore just filled in around it.\n if (pending?.edited && !pending.submitted) writeNow()\n for (const op of pending?.ops ?? []) op(true)\n}\n\n// The root left before this module could connect to it. Nothing was restored\n// into it, so its controls hold what the server rendered plus whatever the\n// user did: forget the draft if the form was submitted; otherwise keep the\n// drafted fields, add the ones the user CHANGED, and run the ops that waited.\nexport function abandon(root, pending) {\n if (!pending) return\n const payload = persistPayload(root)\n if (!payload) return\n if (pending.submitted) return persistRemove(root, payload)\n if (pending.edited) {\n const current = persistRead(root, payload)\n const fields = { ...current?.fields }\n const snapshot = persistSnapshot(root, payload)\n for (const name of changedNames(root, payload)) {\n if (Object.hasOwn(snapshot, name)) fields[name] = snapshot[name]\n }\n persistWrite(root, payload, { fields, state: current?.state ?? null })\n }\n for (const op of pending.ops ?? []) op(false)\n}\n\n// The names of the owned controls the user changed on a root that never\n// restored: those whose live state differs from what the SERVER rendered (the\n// DOM keeps that as defaultValue / defaultChecked / the `selected` attribute), so a\n// server-prefilled value is never mistaken for an edit, and an emptied field\n// or an unticked box counts as one. An editor has no such default: it counts\n// once it holds anything.\nfunction changedNames(root, payload) {\n const names = new Set()\n for (const { el, name, kind } of persistControls(root, payload)) {\n const changed =\n kind === \"editor\"\n ? persistEditorReady(el) && !persistEditorBlank(el)\n : kind === \"contenteditable\"\n ? el.hasAttribute(\"data-reactive-editable\")\n ? shared.text(el) !== shared.served(el)\n : (el.textContent ?? \"\").trim() !== \"\"\n : el.type === \"checkbox\" || el.type === \"radio\"\n ? el.checked !== el.defaultChecked\n : el.tagName === \"SELECT\"\n ? [...el.options].some((option) => option.selected !== option.hasAttribute(\"selected\"))\n : el.value !== el.defaultValue\n if (changed) names.add(name)\n }\n return names\n}\n\n// Flush a pending write while the fields are still readable (Turbo\n// disconnects before leaving the page — a fast visit otherwise loses the last\n// keystrokes), then drop every listener: the closure connect() stored does both.\nexport function disconnect(controller) {\n const off = wired.get(controller)\n wired.delete(controller)\n off?.()\n}\n\n// --- The persist_state / persist_clear client ops --------------------------\n\n// A root that is still waiting for this module shows the server's blanks: an\n// op that snapshots or clears it now would destroy the draft the restore is\n// about to read. Such an op waits in the root's pending record and runs after\n// the restore (connect) — or, if the root leaves first, without one (abandon).\n// Any other root runs it at once.\nfunction whenRestored(root, op) {\n const pending = shared?.waiting.get(root)?.persist\n // (No payload: connect() will not restore this root, so nothing would ever\n // run a queued op — a morph can take the marker away while we load.)\n if (!pending || !persistPayload(root)) return op(true)\n ;(pending.ops ??= []).push(op)\n}\n\nexport function writeState(root, state) {\n whenRestored(root, (restored) => persistWriteState(root, state, restored))\n}\n\nexport function clearRoot(root) {\n whenRestored(root, () => persistClearRoot(root))\n}\n"
|
|
5
|
+
"// Client-only drafts (issue #239) — reactive_persist. A FEATURE MODULE of the\n// reactive controller (issue #275): the core imports it only when a root on\n// the page declares data-reactive-persist, or when a persist_state /\n// persist_clear client op runs.\n//\n// A root that declares\n// data-reactive-persist='{\"key\",\"ttl\",\"debounce\"[,\"fields\"][,\"restore\"]}'\n// keeps a localStorage draft of every persistable OWNED control (write on\n// input/change, restore on connect, clear on a successful submit / TTL / the\n// persist_clear op). The storage layer is MODULE-LEVEL and keyed on the root\n// element — the per-root connect/write path and the persist_state /\n// persist_clear client ops (which receive only the element) share it. Every\n// storage access is try/catch'd: a private window, a quota error or blocked\n// storage degrades to \"no draft\", never a thrown bootstrap. Nothing here\n// leaves the browser: no token, no POST, and phlex-reactive never writes\n// markup into the DOM: native controls are replayed via\n// .value/.checked/.selected, a bare [contenteditable] via textContent, and a\n// rich editor (lexxy-editor, trix-editor — issue #241) through its OWN\n// `value` setter, the same sanitizing import path a paste takes. Never\n// innerHTML.\n//\n// The feature contract (reactive_controller.js \"Feature modules\"):\n// install(shared) once, when the module loads\n// connect(controller, core, morphed, pending) per root connection\n// disconnect(controller, core) before the core's teardown\n// abandon(root, pending) the root left before connect\n// `core` is the controller's handle for features: core.emit(name, detail)\n// raw-dispatches a lifecycle event from the root; core.reseed() re-runs the\n// connect-time seeds that read field values (dirty baseline, show bindings,\n// on-complete arming, filter, tags, nested JSON, compute).\n//\n// TIMING. On the first root of a page that needs it, this module is still on\n// its way when the controller connects, so the restore runs a moment AFTER\n// connect() — and the connect-time seeds, which ran on the server's values,\n// are re-run (core.reseed()). On any later root the module is loaded and the\n// restore runs inside connect() as it always did.\n//\n// While a root waits for this module:\n// * it writes no draft, and its action requests are held back (they would\n// post the server's blanks) — the core's side of it;\n// * the core records, in `pending`, that the user edited a field and that\n// the form was submitted successfully; connect() drafts the edit, or\n// forgets the draft instead of restoring it;\n// * a persist_state / persist_clear op waits in pending.ops — its snapshot\n// would overwrite the draft with those blanks.\n// A root that leaves before the module arrives is handed to abandon().\n\n// The core's shared state (install()): shared.waiting maps a root to\n// { persist: pending } while that root waits for this module.\nlet shared\n\nexport function install(given) {\n shared = given\n}\n\nconst PERSIST_VERSION = 1\nconst PERSIST_PREFIX = \"phlex-reactive:persist:\"\n// Never persisted regardless of author intent: no server default to restore\n// into (hidden, file), secrets (password), and non-value controls.\nconst PERSIST_EXCLUDED_TYPES = new Set([\"hidden\", \"file\", \"password\", \"submit\", \"button\", \"reset\", \"image\"])\nconst PERSIST_NO_VALUE = Symbol(\"persist-no-value\")\nconst PERSIST_STATE_ATTR = \"data-reactive-persist-state\"\n// The editor query the core's #collectFields reads (minus the [name] guard —\n// an editor's name may live on its IDL `name` getter: Trix's `input=`-paired\n// hidden input). The core holds the same two constants (EDITOR_SELECTOR,\n// EDITOR_TAGS); a feature never imports the core, so they are repeated here\n// and spec/javascript/reactive_persist.test.js pins that they stay equal.\nexport const PERSIST_EDITOR_SELECTOR =\n \":is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])\"\nexport const PERSIST_EDITOR_TAGS = new Set([\"lexxy-editor\", \"trix-editor\"])\n// An editor's own chrome: Lexxy renders its toolbar (a `lexxy-code-language`\n// select, the link dialog's `href` input) INSIDE <lexxy-editor>, Trix as a\n// sibling <trix-toolbar>. Those are named native controls that are not the\n// user's fields — never drafted, never restored into.\nconst PERSIST_EDITOR_CHROME = \"lexxy-editor, trix-editor, trix-toolbar\"\n// The editors' own bubbling change events — the keystroke signal for the\n// draft write, since neither lets its contenteditable's native `input` bubble.\nconst PERSIST_EDITOR_CHANGE_EVENTS = [\"lexxy:change\", \"trix-change\"]\n// \"Is this editor empty\" when it exposes no predicate of its own (Lexxy's\n// `isEmpty`, Trix's `editor.getDocument().isEmpty()`): Lexxy's own empty\n// list plus the Trix / contenteditable empties. Exact strings, no HTML\n// parsing — an attachment-only body stays non-blank.\nconst PERSIST_EMPTY_HTML = new Set([\"\", \"<p></p>\", \"<p><br></p>\", \"<div><br></div>\"])\n// Once-per-root guards: a malformed payload warns once; the storage-failure\n// and editor-restore dev notes (debug mode only) print once each.\nconst persistPayloadWarned = new WeakSet()\nconst persistFailureNoted = new WeakSet()\nconst persistEditorNoted = new WeakSet()\n\n// The root's parsed payload, or null (undeclared / malformed → warned once).\n// A control-level \"off\" (reactive_persist_skip) is never a root payload.\nfunction persistPayload(root) {\n const raw = root?.getAttribute?.(\"data-reactive-persist\")\n if (!raw || raw === \"off\") return null\n try {\n const payload = JSON.parse(raw)\n if (payload && typeof payload === \"object\" && typeof payload.key === \"string\" && payload.key !== \"\") return payload\n } catch {\n // fall through to the warn\n }\n if (!persistPayloadWarned.has(root)) {\n persistPayloadWarned.add(root)\n console.warn(`[phlex-reactive] malformed reactive_persist payload ${JSON.stringify(raw)} — persistence disabled`)\n }\n return null\n}\n\nfunction persistStorage() {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage\n } catch {\n return null // the accessor itself can throw (blocked site data)\n }\n}\n\n// The dev lens for \"why did nothing come back\": ONLY under data-reactive-debug\n// (Phlex::Reactive.debug), once per root — production stays silent.\nfunction persistNoteFailure(root, error) {\n if (root?.getAttribute?.(\"data-reactive-debug\") !== \"true\" || persistFailureNoted.has(root)) return\n persistFailureNoted.add(root)\n console.info(`[phlex-reactive] reactive_persist: storage unavailable — draft skipped (${error?.name ?? error})`)\n}\n\nfunction persistKeyFor(payload) {\n return PERSIST_PREFIX + payload.key\n}\n\n// Read + validate the draft: null when absent, unparsable, another schema\n// version, or expired (an expired draft is REMOVED on read). Returns\n// { fields, state } with state null when the draft carries none.\nfunction persistRead(root, payload) {\n const store = persistStorage()\n if (!store) return null\n let raw\n try {\n raw = store.getItem(persistKeyFor(payload))\n } catch (error) {\n persistNoteFailure(root, error)\n return null\n }\n if (!raw) return null\n let draft\n try {\n draft = JSON.parse(raw)\n } catch {\n return null\n }\n if (!draft || typeof draft !== \"object\" || draft.v !== PERSIST_VERSION) return null\n const ttlMs = Number(payload.ttl) * 1000\n if (!(Number(draft.savedAt) + ttlMs > Date.now())) {\n persistRemove(root, payload)\n return null\n }\n const fields = draft.fields && typeof draft.fields === \"object\" ? draft.fields : {}\n const state = draft.state && typeof draft.state === \"object\" ? draft.state : null\n return { fields, state }\n}\n\nfunction persistWrite(root, payload, { fields, state }) {\n const store = persistStorage()\n if (!store) return false\n const draft = { v: PERSIST_VERSION, savedAt: Date.now(), fields }\n if (state) draft.state = state\n try {\n store.setItem(persistKeyFor(payload), JSON.stringify(draft))\n return true\n } catch (error) {\n persistNoteFailure(root, error)\n return false\n }\n}\n\nfunction persistRemove(root, payload) {\n root?.removeAttribute?.(PERSIST_STATE_ATTR)\n const store = persistStorage()\n if (!store) return\n try {\n store.removeItem(persistKeyFor(payload))\n } catch (error) {\n persistNoteFailure(root, error)\n }\n}\n\n// The persistable controls this root OWNS (#15: a nested reactive root's\n// controls are its own), minus the excluded types, the reactive_persist_skip\n// marker, and — when `fields` narrows the set — any undeclared name. Each\n// entry is { el, name, kind } with kind \"native\" (input/select/textarea),\n// \"editor\" (lexxy-editor / trix-editor) or \"contenteditable\" (a bare named\n// editable element) — issue #241.\nfunction persistControls(root, payload) {\n const allow = Array.isArray(payload.fields) ? new Set(payload.fields) : null\n const out = []\n const owned = (el) =>\n el.closest('[data-controller~=\"reactive\"]') === root && el.getAttribute(\"data-reactive-persist\") !== \"off\"\n for (const el of root.querySelectorAll(\"input[name], select[name], textarea[name]\")) {\n if (!owned(el) || PERSIST_EXCLUDED_TYPES.has(el.type) || el.closest(PERSIST_EDITOR_CHROME)) continue\n if (allow && !allow.has(el.name)) continue\n out.push({ el, name: el.name, kind: \"native\" })\n }\n for (const el of root.querySelectorAll(PERSIST_EDITOR_SELECTOR)) {\n if (!owned(el)) continue\n const name = persistEditorName(el)\n if (!name || (allow && !allow.has(name))) continue\n out.push({ el, name, kind: PERSIST_EDITOR_TAGS.has(el.localName) ? \"editor\" : \"contenteditable\" })\n }\n return out\n}\n\n// A contenteditable's text: a reactive_editable through the core's collector\n// (issue #351 — line breaks kept, as the request sends it), a bare one as its\n// textContent (#241).\nfunction persistText(el) {\n return el.hasAttribute(\"data-reactive-editable\") ? shared.text(el) : (el.textContent ?? \"\")\n}\n\n// The attribute first (Lexxy, a bare contenteditable — which has no `name`\n// IDL property at all), then the IDL getter (Trix resolves it through its\n// `input=`-paired hidden input, which is itself excluded as type=hidden).\nfunction persistEditorName(el) {\n return el.getAttribute(\"name\") || (typeof el.name === \"string\" && el.name) || null\n}\n\n// An editor is READY once its custom element has upgraded and connected: only\n// then does it expose the string `value` accessor (Lexxy's setter throws\n// before connectedCallback created its editor; Trix's discards the value).\nfunction persistEditorReady(el) {\n return typeof el.value === \"string\"\n}\n\n// Ask the editor whether it is empty (Lexxy `isEmpty`; Trix\n// `editor.getDocument().isEmpty()`), else the exact-string fallback. An\n// attachment-only server body is therefore NON-blank and never overwritten.\nfunction persistEditorBlank(el) {\n if (typeof el.isEmpty === \"boolean\") return el.isEmpty\n const doc = el.editor?.getDocument?.()\n if (typeof doc?.isEmpty === \"function\") return doc.isEmpty()\n return PERSIST_EMPTY_HTML.has(el.value.trim())\n}\n\n// The editor-restore dev lens: ONLY under data-reactive-debug, once per root.\nfunction persistNoteEditorFailure(root, name, error) {\n if (root?.getAttribute?.(\"data-reactive-debug\") !== \"true\" || persistEditorNoted.has(root)) return\n persistEditorNoted.add(root)\n console.info(\n `[phlex-reactive] reactive_persist: could not restore editor ${JSON.stringify(name)} — ${error?.message ?? error}`,\n )\n}\n\nfunction persistSelectMultiple(el) {\n return el.tagName === \"SELECT\" && el.multiple\n}\n\n// How many controls can contribute a value under each `[]` name, counted the\n// way persistSnapshot fills the slot — a radio is the exception there and here.\n// A group of ONE is unambiguous: its single entry can only have come from that\n// control.\nfunction persistGroupSizes(controls) {\n const sizes = new Map()\n for (const { el, name } of controls) {\n if (el.type === \"radio\" || !String(name).endsWith(\"[]\")) continue\n sizes.set(name, (sizes.get(name) ?? 0) + 1)\n }\n return sizes\n}\n\n// The value a control that cannot pick its own entry out of a list may take.\n// A list belongs to a `[]` group, and in a group of two or more nothing says\n// which entry came from which control — restoring it would paste\n// \"freeform,news\" into a text field. A group of ONE has no such ambiguity, and\n// refusing it would silently drop the draft of a plain field whose name merely\n// ends in `[]` (a list JS maintains), which worked before groups existed.\n// Returns PERSIST_NO_VALUE when the control must keep what the server rendered.\nfunction persistGenericValue(value, name, sizes) {\n if (!Array.isArray(value)) return value\n if (sizes.get(name) !== 1 || value.length !== 1) return PERSIST_NO_VALUE\n\n return value[0]\n}\n\n// Snapshot the owned controls: radio → the checked value (null when the group\n// has none, so a restore leaves it alone), checkbox → checked, multi-select →\n// the selected values, a rich editor → its serialized `value` (omitted while\n// the element hasn't upgraded — never a phantom \"\"), a bare contenteditable →\n// its textContent, else .value. Mirrors the core's #collectFields reads.\nfunction persistSnapshot(root, payload) {\n const fields = {}\n // A `[]` name is a group for every control that can contribute a value —\n // checkboxes, selects, text inputs, editors and contenteditables all append\n // to one array, mirroring #collectFields. The one exception is a RADIO group,\n // which means \"pick one\" and keeps its single value with or without the\n // suffix, exactly as the collector treats it.\n //\n // Reading the slot without this (fields[name] ?? []) breaks as soon as a\n // non-checkbox shares the group's name: a text input or an editor leaves a\n // string there, `.push` on it throws inside the draft write, and that write\n // is swallowed — the root then persists nothing at all, silently. Editors\n // are collected AFTER the native controls, so they always land last.\n const groupSlot = (name) => {\n const existing = fields[name]\n return Array.isArray(existing) ? existing : (fields[name] = [])\n }\n for (const { el, name, kind } of persistControls(root, payload)) {\n const group = String(name).endsWith(\"[]\")\n if (kind === \"editor\") {\n if (persistEditorReady(el)) {\n if (group) groupSlot(name).push(el.value)\n else fields[name] = el.value\n }\n } else if (kind === \"contenteditable\") {\n const text = persistText(el)\n if (group) groupSlot(name).push(text)\n else fields[name] = text\n } else if (el.type === \"radio\") {\n if (el.checked) fields[name] = el.value\n else if (!Object.hasOwn(fields, name)) fields[name] = null\n } else if (el.type === \"checkbox\") {\n // A `[]` group drafts the list of ticked values, mirroring #collectFields\n // (issue #258). Without this the boxes overwrote each other and the draft\n // held one boolean, which the restore then applied to every box of the\n // group. A lone checkbox keeps the boolean it has always been.\n if (group) {\n const slot = groupSlot(name)\n if (el.checked) slot.push(el.value)\n } else {\n fields[name] = el.checked\n }\n } else if (persistSelectMultiple(el)) {\n const selected = [...el.options].filter((o) => o.selected).map((o) => o.value)\n if (group) groupSlot(name).push(...selected)\n else fields[name] = selected\n } else if (group) {\n groupSlot(name).push(el.value)\n } else {\n fields[name] = el.value\n }\n }\n return fields\n}\n\n// Replay the draft into the owned controls. Default (restore: blank): a\n// control the server rendered NON-BLANK keeps its value — a 422 re-render's\n// submitted values beat an older draft. restore: \"always\" lets the draft win.\n// Values land via .value/.checked/.selected, textContent, or the editor's own\n// `value` setter — never HTML written by us.\nfunction persistApply(root, payload, fields) {\n const always = payload.restore === \"always\"\n const controls = persistControls(root, payload)\n // Which group names the SERVER rendered with a box already ticked. Computed\n // BEFORE the loop on purpose: the loop writes `checked`, so asking this\n // question from inside it would read THIS restore's own work — the first box\n // it ticks makes every later box of the same group look server-rendered, and\n // a draft of two values comes back as one. The radio branch below asks the\n // same question inline and stays correct only because a radio group holds a\n // single value.\n const groupSizes = persistGroupSizes(controls)\n const serverTicked = new Set()\n for (const control of controls) {\n if (control.kind === \"native\" && control.el.type === \"checkbox\" && control.el.checked) {\n serverTicked.add(control.name)\n }\n }\n for (const { el, name, kind } of controls) {\n if (!Object.hasOwn(fields, name)) continue\n let value = fields[name]\n if (value === null || value === undefined) continue\n // A multi-select reads a list by matching option values, which is only\n // sound when the list is ITS list. In a group with another contributor the\n // entries are mixed, and a text value that happens to equal an option\n // would select it — measured, a draft of [\"blue\",\"freitext\"] from a select\n // plus a text field selected both options. `?? 0` because a name without\n // the suffix is not in the map at all, and a plain `<select multiple\n // name=\"colors\">` must keep restoring.\n if (Array.isArray(value) && persistSelectMultiple(el) && (groupSizes.get(name) ?? 0) > 1) continue\n\n // An array belongs to a `[]` group, and only a control that can pick ITS\n // entry out of the list may read it: a checkbox matches by value, a\n // multi-select by option. Everything else — editors, contenteditables,\n // text inputs — keeps what the server rendered, because the list does not\n // record which entry came from which control. This sits ABOVE the branch\n // chain on purpose: below the editor branch it would never fire for the\n // very controls that land last in the snapshot.\n if (Array.isArray(value) && !(el.type === \"checkbox\" || persistSelectMultiple(el))) {\n value = persistGenericValue(value, name, groupSizes)\n if (value === PERSIST_NO_VALUE) continue\n }\n // The mirror, for a draft written BEFORE a group was drafted as a list\n // (#258): there `features[]` held ONE boolean, and applying it here ticks\n // every box of the group — precisely the state this fix removes, for as\n // long as the draft lives (default ttl 7 days). A group key that is not a\n // list is stale by definition, so the control keeps what the server\n // rendered and the next snapshot overwrites the key. It reads for a\n // multi-select too: 0.13.2 wrote last-writer-wins per name, so a checkbox\n // in a mixed group could leave its boolean under the select's name, and\n // under `restore: \"always\"` the select would then deselect everything —\n // `wanted` being Set{\"true\"} matches no option. Asking for the `[]` suffix\n // is what leaves a lone `gift` checkbox on the boolean it has always held —\n // and scoping the rule to the one key whose meaning changed is why\n // PERSIST_VERSION stays at 1: bumping it would also throw away the drafted\n // prose of every form that has no checkbox group at all.\n if ((el.type === \"checkbox\" || persistSelectMultiple(el)) && String(name).endsWith(\"[]\") && !Array.isArray(value)) {\n continue\n }\n if (kind === \"editor\") {\n persistApplyEditor(root, el, name, value, always)\n } else if (kind === \"contenteditable\") {\n if (!always && persistText(el).trim() !== \"\") continue\n el.textContent = String(value)\n } else if (el.type === \"radio\") {\n if (!always && controls.some((c) => c.kind === \"native\" && c.el.type === \"radio\" && c.name === name && c.el.checked)) continue\n el.checked = el.value === String(value)\n } else if (el.type === \"checkbox\" && Array.isArray(value)) {\n // A drafted group ticks exactly the boxes it held. One box the SERVER\n // rendered ticked means it had a say, and the draft yields for the whole\n // group.\n if (!always && serverTicked.has(name)) continue\n el.checked = value.map(String).includes(el.value)\n } else if (el.type === \"checkbox\") {\n if (!always && el.checked) continue\n el.checked = Boolean(value)\n } else if (persistSelectMultiple(el)) {\n if (!always && [...el.options].some((o) => o.selected)) continue\n const wanted = new Set((Array.isArray(value) ? value : [value]).map(String))\n for (const o of el.options) o.selected = wanted.has(o.value)\n } else {\n if (!always && el.value !== \"\") continue\n el.value = String(value)\n }\n }\n persistDeferEditors(root, payload, fields)\n}\n\n// A ready editor takes the value through its own setter; the setter is the\n// editor's sanitizing import (Trix HTMLParser, Lexxy $generateNodesFromDOM +\n// sanitizer). A throw (Lexxy before its editor exists) never escapes connect.\nfunction persistApplyEditor(root, el, name, value, always) {\n // A list never reaches here: both callers resolve it through\n // persistGenericValue first — the group of two or more has no mapping back to\n // this editor, the group of one does. Belt and braces, because this is the\n // one apply path with a second entry point.\n if (Array.isArray(value)) return\n if (!persistEditorReady(el)) return // not upgraded yet — persistDeferEditors re-applies after define\n if (!always && !persistEditorBlank(el)) return\n try {\n el.value = String(value)\n } catch (error) {\n persistNoteEditorFailure(root, name, error)\n }\n}\n\n// An editor whose custom element is not defined yet (Trix defines its elements\n// in a setTimeout after load; a lazily imported Lexxy) cannot take a value now.\n// Re-apply per TAG once it is defined — re-querying the controls (the upgrade\n// may replace the node) and re-checking restore: blank at that moment — unless\n// the root has left the document meanwhile.\nfunction persistDeferEditors(root, payload, fields) {\n const registry = globalThis.customElements\n if (typeof registry?.whenDefined !== \"function\") return\n const pending = new Set()\n for (const el of root.querySelectorAll(\"lexxy-editor, trix-editor\")) {\n if (!persistEditorReady(el) && !registry.get?.(el.localName)) pending.add(el.localName)\n }\n const always = payload.restore === \"always\"\n for (const tag of pending) {\n registry.whenDefined(tag).then(() => {\n if (!root.isConnected) return\n const deferred = persistControls(root, payload)\n const sizes = persistGroupSizes(deferred)\n for (const { el, name, kind } of deferred) {\n if (kind !== \"editor\" || el.localName !== tag || !Object.hasOwn(fields, name)) continue\n const value = persistGenericValue(fields[name], name, sizes)\n if (value === null || value === undefined || value === PERSIST_NO_VALUE) continue\n persistApplyEditor(root, el, name, value, always)\n }\n })\n }\n}\n\n// The persist_state op body: merge a FLAT bag into the root's draft (re-\n// snapshotting the fields so the write is whole) and mirror it on the root.\n// A root without reactive_persist is a call-site bug — warn and skip.\n// `restored` false: the root never restored (it left before this module\n// arrived), so its controls hold the server's blanks — keep the drafted\n// fields and merge the bag only.\nfunction persistWriteState(root, state, restored = true) {\n const payload = persistPayload(root)\n if (!payload) {\n console.warn(\"[phlex-reactive] persist_state on a root without reactive_persist — skipped\")\n return\n }\n if (!state || typeof state !== \"object\") return\n const current = persistRead(root, payload)\n const merged = { ...(current?.state ?? {}), ...state }\n const fields = restored ? persistSnapshot(root, payload) : (current?.fields ?? {})\n if (persistWrite(root, payload, { fields, state: merged })) {\n root.setAttribute?.(PERSIST_STATE_ATTR, JSON.stringify(merged))\n }\n}\n\nfunction persistClearRoot(root) {\n const payload = persistPayload(root)\n if (payload) persistRemove(root, payload)\n}\n\n// --- Per-root wiring -------------------------------------------------------\n\n// controller -> the closure that flushes and unwires the connection this\n// feature is wired to. Its timer and handlers are locals of connect(), not a\n// record's fields: the minifier renames a local, never a property (#310).\nconst wired = new WeakMap()\n\n// document -> the submit-end handlers of its persisted roots. One document\n// listener serves them all: a listener per root made connecting and\n// disconnecting N persisted roots quadratic (the browser scans a target's\n// listeners on each add and remove). Added with the first root, removed with\n// the last. Returns the handler's removal.\nconst submitEnds = new WeakMap()\n\nfunction onSubmitEndOf(doc, handler) {\n if (typeof doc?.addEventListener !== \"function\") return () => {}\n let shared = submitEnds.get(doc)\n if (!shared) {\n const handlers = new Set()\n const listener = (event) => {\n for (const run of handlers) run(event)\n }\n doc.addEventListener(\"turbo:submit-end\", listener)\n shared = [handlers, listener]\n submitEnds.set(doc, shared)\n }\n const [handlers, listener] = shared\n handlers.add(handler)\n return () => {\n handlers.delete(handler)\n if (handlers.size > 0) return\n doc.removeEventListener?.(\"turbo:submit-end\", listener)\n submitEnds.delete(doc)\n }\n}\n\n// Restore the draft into the owned controls, expose the state bag, announce,\n// then arm the write listeners. The restore reads ONCE and never writes back:\n// nothing is wired until it has completed, so no listener can clobber the\n// draft with the server's blanks. The submit-end listener is DOCUMENT-level\n// (the event fires on the form, which is usually an ANCESTOR of this root)\n// and gated on the form containing this root.\n//\n// `pending` is what the core recorded while this root waited for the module\n// (undefined when it never had to): see TIMING at the top.\nexport function connect(controller, core, _morphed, pending) {\n const root = controller.element\n const payload = persistPayload(root)\n if (!payload) return\n // Submitted successfully while we were on our way: the draft is done with.\n if (pending?.submitted) persistRemove(root, payload)\n const draft = persistRead(root, payload)\n if (draft) {\n persistApply(root, payload, draft.fields)\n if (draft.state) root.setAttribute?.(PERSIST_STATE_ATTR, JSON.stringify(draft.state))\n core.emit(\"reactive:persist-restored\", { key: payload.key, fields: draft.fields, state: draft.state ?? {} })\n // If the connect-time seeds ran before this restore, they ran on the\n // server's values; they read fields.\n core.reseed()\n }\n\n // ONE pending keystroke write per root (a snapshot is a full pass, so\n // per-field timers would only multiply writes).\n let timer = null\n const cancel = () => {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n // Snapshot every persistable owned control and write. Re-reads the current\n // draft first so a state bag written by persist_state survives the write\n // (the bag lives in storage, not on the connection — the op has none).\n const writeNow = () => {\n cancel()\n const current = persistRead(root, payload)\n persistWrite(root, payload, { fields: persistSnapshot(root, payload), state: current?.state ?? null })\n }\n // Trailing-edge debounce for keystrokes.\n const onInput = () => {\n const ms = Number(payload.debounce) || 0\n if (ms <= 0) return writeNow()\n if (timer !== null) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n writeNow()\n }, ms)\n }\n // A SUCCESSFUL Turbo form submission of the form that owns this root\n // forgets the draft. tagName (not instanceof) so a cross-realm form counts.\n const onSubmitEnd = (event) => {\n if (!event?.detail?.success) return\n const form = event.target\n if (form?.tagName !== \"FORM\" || typeof form.contains !== \"function\") return\n if (!form.contains(root)) return\n // Drop a pending keystroke write too — the disconnect flush that follows\n // Turbo's redirect visit would otherwise resurrect the just-cleared draft.\n cancel()\n persistRemove(root, payload)\n }\n // Rich editors (#241): Lexical and Trix swallow the native `input` of\n // their contenteditable, so their own bubbling change events are the\n // keystroke signal — same trailing-edge debounce as `input`.\n const inputs = [\"input\", ...PERSIST_EDITOR_CHANGE_EVENTS]\n for (const event of inputs) root.addEventListener?.(event, onInput)\n root.addEventListener?.(\"change\", writeNow)\n const offSubmitEnd = onSubmitEndOf(globalThis.document, onSubmitEnd)\n wired.set(controller, () => {\n if (timer !== null) writeNow()\n for (const event of inputs) root.removeEventListener?.(event, onInput)\n root.removeEventListener?.(\"change\", writeNow)\n offSubmitEnd()\n })\n\n // An edit made while we were on our way had no listener to draft it: draft\n // it now, with everything the restore just filled in around it.\n if (pending?.edited && !pending.submitted) writeNow()\n for (const op of pending?.ops ?? []) op(true)\n}\n\n// The root left before this module could connect to it. Nothing was restored\n// into it, so its controls hold what the server rendered plus whatever the\n// user did: forget the draft if the form was submitted; otherwise keep the\n// drafted fields, add the ones the user CHANGED, and run the ops that waited.\nexport function abandon(root, pending) {\n if (!pending) return\n const payload = persistPayload(root)\n if (!payload) return\n if (pending.submitted) return persistRemove(root, payload)\n if (pending.edited) {\n const current = persistRead(root, payload)\n const fields = { ...current?.fields }\n const snapshot = persistSnapshot(root, payload)\n for (const name of changedNames(root, payload)) {\n if (Object.hasOwn(snapshot, name)) fields[name] = snapshot[name]\n }\n persistWrite(root, payload, { fields, state: current?.state ?? null })\n }\n for (const op of pending.ops ?? []) op(false)\n}\n\n// The names of the owned controls the user changed on a root that never\n// restored: those whose live state differs from what the SERVER rendered (the\n// DOM keeps that as defaultValue / defaultChecked / the `selected` attribute), so a\n// server-prefilled value is never mistaken for an edit, and an emptied field\n// or an unticked box counts as one. An editor has no such default: it counts\n// once it holds anything.\nfunction changedNames(root, payload) {\n const names = new Set()\n for (const { el, name, kind } of persistControls(root, payload)) {\n const changed =\n kind === \"editor\"\n ? persistEditorReady(el) && !persistEditorBlank(el)\n : kind === \"contenteditable\"\n ? el.hasAttribute(\"data-reactive-editable\")\n ? shared.text(el) !== shared.served(el)\n : (el.textContent ?? \"\").trim() !== \"\"\n : el.type === \"checkbox\" || el.type === \"radio\"\n ? el.checked !== el.defaultChecked\n : el.tagName === \"SELECT\"\n ? [...el.options].some((option) => option.selected !== option.hasAttribute(\"selected\"))\n : el.value !== el.defaultValue\n if (changed) names.add(name)\n }\n return names\n}\n\n// Flush a pending write while the fields are still readable (Turbo\n// disconnects before leaving the page — a fast visit otherwise loses the last\n// keystrokes), then drop every listener: the closure connect() stored does both.\nexport function disconnect(controller) {\n const off = wired.get(controller)\n wired.delete(controller)\n off?.()\n}\n\n// --- The persist_state / persist_clear client ops --------------------------\n\n// A root that is still waiting for this module shows the server's blanks: an\n// op that snapshots or clears it now would destroy the draft the restore is\n// about to read. Such an op waits in the root's pending record and runs after\n// the restore (connect) — or, if the root leaves first, without one (abandon).\n// Any other root runs it at once.\nfunction whenRestored(root, op) {\n const pending = shared?.waiting.get(root)?.persist\n // (No payload: connect() will not restore this root, so nothing would ever\n // run a queued op — a morph can take the marker away while we load.)\n if (!pending || !persistPayload(root)) return op(true)\n ;(pending.ops ??= []).push(op)\n}\n\nexport function writeState(root, state) {\n whenRestored(root, (restored) => persistWriteState(root, state, restored))\n}\n\nexport function clearRoot(root) {\n whenRestored(root, () => persistClearRoot(root))\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "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",
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2D40FF088B985FEC64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|