phlex-reactive 0.11.0 → 0.11.2
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 +58 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +161 -7
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/helpers.rb +7 -1
- data/lib/phlex/reactive/inspector.rb +5 -1
- data/lib/phlex/reactive/test_helpers/system.rb +105 -0
- data/lib/phlex/reactive/test_helpers.rb +6 -0
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +5 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac1f001e762b40825811633d86a7cb9e050b85d32a7f7c11a44196b48f486af6
|
|
4
|
+
data.tar.gz: a969a678bf9f0d293b953529f9c31eb4871263dc391477e4301d3fedd5cec6fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8378be9d18bd169e2f78dfbd5171931e952ecbb875c74df0ba5328518ea0a74c641e7840807a7d6eb216d647ecd0daa6f32cdf70468d049e6ed47fa2d0169903
|
|
7
|
+
data.tar.gz: 7ec7fb3754b137191feb9d38dca9ef9b14b6993da9909dd10ea0f25b661660259c3839b4fa011982dec206d687b765b0a70e7d762ca62c09ed22cbf7f055dba0
|
data/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,66 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A global reactive-activity signal — the analogue of Turbo's progress bar (#201).**
|
|
12
|
+
The client now maintains a document-level count of in-flight reactive operations
|
|
13
|
+
across ALL roots — incremented when a dispatch round trip or a deferred render
|
|
14
|
+
starts, decremented when it settles — and exposes it two ways:
|
|
15
|
+
- a marker on `<html>`: `data-reactive-active` is present while the count is `> 0`,
|
|
16
|
+
so CSS can drive a global spinner and code (or a test) can read "is the reactive
|
|
17
|
+
layer settling?" without knowing about any individual root;
|
|
18
|
+
- events on `document`: `reactive:busy` fires on the `0 → >0` edge and
|
|
19
|
+
`reactive:idle` on the `>0 → 0` edge (edges only, each carrying `{ count }`), so
|
|
20
|
+
an app can flip a spinner or gate an "unsaved changes" guard globally.
|
|
21
|
+
|
|
22
|
+
Purely additive — every per-root marker/event (`data-reactive-busy`, `aria-busy`,
|
|
23
|
+
`data-reactive-defer-pending`) is unchanged. A distinct attribute name
|
|
24
|
+
(`data-reactive-active`, not the per-root `data-reactive-busy`) keeps the
|
|
25
|
+
document-level signal unambiguous. `compute_seed` is not counted: `recompute()` is
|
|
26
|
+
synchronous, so a seed settles inline with no async window to await.
|
|
27
|
+
|
|
28
|
+
- **`Phlex::Reactive::TestHelpers::System` — system/browser test helpers built on the
|
|
29
|
+
activity signal (#201).** Opt-in and loaded only when Capybara is present (the same
|
|
30
|
+
optional-require gate as the RSpec matchers), so it stays entirely out of the
|
|
31
|
+
runtime path. Mix it into system examples:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
RSpec.configure do |c|
|
|
35
|
+
c.include Phlex::Reactive::TestHelpers::System, type: :system
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `wait_for_reactive` — block until the layer is idle (every round trip AND deferred
|
|
40
|
+
render settled, the `<html>` marker cleared). The system-test twin of
|
|
41
|
+
`wait_for_turbo`, which watches the Turbo progress bar, not a reactive morph/seed.
|
|
42
|
+
- `have_reactive_value(id, value)` / `have_reactive_text(id, value)` — assert a field
|
|
43
|
+
or a mirror/recap node by RE-RESOLVING it by id every poll, so a `reactive_compute`
|
|
44
|
+
re-seed or a morph that replaces the node after the action returns can never raise
|
|
45
|
+
`StaleReferenceError` or read a transient blank — the matcher waits for the value
|
|
46
|
+
to settle.
|
|
47
|
+
|
|
9
48
|
### Fixed
|
|
10
49
|
|
|
50
|
+
- **A freshly-rendered `reactive_compute` root now self-seeds its derived fields on connect (#199).**
|
|
51
|
+
Before, a compute root computed nothing until the first user `input` — a fresh render
|
|
52
|
+
(or a server validation-error re-render that replaced the body) left every derived
|
|
53
|
+
output and cross-root `mirror:` node blank until you typed. Apps worked around it by
|
|
54
|
+
dispatching a synthetic seed `input` on connect, but the compute root is a distinct
|
|
55
|
+
Stimulus controller that may connect a frame later, so the seed raced its own wiring —
|
|
56
|
+
the reported symptom being a PARTIAL apply (an early output painted; a later output
|
|
57
|
+
and the mirror stayed blank). Now `connect()` runs ONE full `recompute()` after the
|
|
58
|
+
controller is fully connected, so the whole single-pass write set (batch outputs →
|
|
59
|
+
paint text sinks + cross-root mirrors → dispatch) runs synchronously and every
|
|
60
|
+
declared output, text sink, and mirror paints from a single reducer result — no user
|
|
61
|
+
interaction, no round trip. It re-seeds on `turbo:morph-element` (the show/filter/dirty
|
|
62
|
+
precedent) and is idempotent (change-guarded writes make a re-seed a no-op, so an app
|
|
63
|
+
still dispatching a synthetic input is harmless). The seed is gated on a new
|
|
64
|
+
`data-reactive-compute-seed` marker emitted by `reactive_root(compute:)`; a non-compute
|
|
65
|
+
root pays one attribute read and never seeds. The seed passes no event, so
|
|
66
|
+
`meta.changed` is `null` — the correct "no field edited yet" semantics, so a convergent
|
|
67
|
+
reducer's default branch computes the full settled set.
|
|
68
|
+
|
|
11
69
|
- **Scoped form fields no longer silently drop their params — the #67 footgun is fixed (#184).**
|
|
12
70
|
Under `reactive_scope`, `reactive_field(:date)` now emits the scoped wire name
|
|
13
71
|
(`name="invoice[date]"`), so the POST arrives bracketed — and the endpoint unwraps
|
|
@@ -125,9 +125,28 @@ export function registerReactiveJs() {
|
|
|
125
125
|
// keystrokes can never paint stale totals over fresh ones.
|
|
126
126
|
const pendingDefers = new Map()
|
|
127
127
|
|
|
128
|
+
// Register/drop a target's pending defer entry, keeping the GLOBAL activity
|
|
129
|
+
// counter (issue #201) balanced against the Map's ACTUAL key presence — a defer
|
|
130
|
+
// is one in-flight reactive operation for as long as its registry entry lives.
|
|
131
|
+
// Keying enter/exit on the presence TRANSITION (not the raw call) means a
|
|
132
|
+
// re-set of an already-present key, or a delete of an absent one, can never
|
|
133
|
+
// unbalance the count. Both the fetch (pull) and stream (push) lane route their
|
|
134
|
+
// registry mutations through here, so the push lane is counted correctly even
|
|
135
|
+
// though its DOM pending markers clear by a node swap, not clearDeferPending.
|
|
136
|
+
function setPendingDefer(targetId, entry) {
|
|
137
|
+
const isNew = !pendingDefers.has(targetId)
|
|
138
|
+
pendingDefers.set(targetId, entry)
|
|
139
|
+
if (isNew) enterReactiveActivity()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function deletePendingDefer(targetId) {
|
|
143
|
+
if (pendingDefers.delete(targetId)) exitReactiveActivity()
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
// Test seam: clear the module-level registry between unit tests. Also resets
|
|
129
147
|
// the one-shot settle-listener guard so a test's fresh document re-registers
|
|
130
|
-
// the turbo:before-stream-render settler.
|
|
148
|
+
// the turbo:before-stream-render settler. Does NOT touch the activity counter —
|
|
149
|
+
// resetReactiveActivity is its own seam (the two are reset together in tests).
|
|
131
150
|
export function resetReactiveDefers() {
|
|
132
151
|
pendingDefers.clear()
|
|
133
152
|
deferStreamSettleRegistered = false
|
|
@@ -184,7 +203,7 @@ function settleStreamDeferOnRender(event) {
|
|
|
184
203
|
? target.slice("reactive-defer-src-".length)
|
|
185
204
|
: target
|
|
186
205
|
const entry = pendingDefers.get(targetId)
|
|
187
|
-
if (entry?.via === "stream")
|
|
206
|
+
if (entry?.via === "stream") deletePendingDefer(targetId)
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
// The pull lane: mark the target pending and POST the signed defer token to
|
|
@@ -198,7 +217,7 @@ function startFetchDefer(targetId, token) {
|
|
|
198
217
|
supersedeDefer(targetId)
|
|
199
218
|
markDeferPending(el)
|
|
200
219
|
const entry = { via: "fetch", abort: new AbortController(), timedOut: false }
|
|
201
|
-
|
|
220
|
+
setPendingDefer(targetId, entry)
|
|
202
221
|
performDeferFetch(targetId, entry, token)
|
|
203
222
|
}
|
|
204
223
|
|
|
@@ -248,7 +267,7 @@ function startStreamDefer(targetId, directive) {
|
|
|
248
267
|
// re-finds the element by id instead. The entry is dropped on
|
|
249
268
|
// supersession or when the arriving broadcast replaces the target (a
|
|
250
269
|
// turbo:before-stream-render hook, below).
|
|
251
|
-
|
|
270
|
+
setPendingDefer(targetId, { via: "stream" })
|
|
252
271
|
}
|
|
253
272
|
|
|
254
273
|
// The deterministic id of a target's one-shot <pgbus-stream-source>.
|
|
@@ -332,7 +351,7 @@ async function performDeferFetch(targetId, entry, token) {
|
|
|
332
351
|
function supersedeDefer(targetId) {
|
|
333
352
|
const existing = pendingDefers.get(targetId)
|
|
334
353
|
if (!existing) return
|
|
335
|
-
|
|
354
|
+
deletePendingDefer(targetId)
|
|
336
355
|
if (existing.via === "fetch") existing.abort.abort()
|
|
337
356
|
// Stream lane: re-find the old source by its deterministic id and remove it
|
|
338
357
|
// (unsubscribe) — we deliberately don't hold a strong ref to the detached
|
|
@@ -353,7 +372,7 @@ function clearDeferPending(el) {
|
|
|
353
372
|
// Success/204: drop the registry entry, clear pending, and clear any prior
|
|
354
373
|
// defer failure marker (recovery resets error-driven CSS, issue #100 style).
|
|
355
374
|
function settleDefer(targetId) {
|
|
356
|
-
|
|
375
|
+
deletePendingDefer(targetId)
|
|
357
376
|
const el = document.getElementById(targetId)
|
|
358
377
|
if (!el) return
|
|
359
378
|
clearDeferPending(el)
|
|
@@ -365,7 +384,7 @@ function settleDefer(targetId) {
|
|
|
365
384
|
// reactive:error whose retry() re-enters the defer fetch with the SAME token
|
|
366
385
|
// (still valid inside the TTL; an expired token 400s into this same path).
|
|
367
386
|
function failDefer(targetId, token, status) {
|
|
368
|
-
|
|
387
|
+
deletePendingDefer(targetId)
|
|
369
388
|
const el = document.getElementById(targetId)
|
|
370
389
|
if (!el) return
|
|
371
390
|
clearDeferPending(el)
|
|
@@ -560,6 +579,87 @@ export function __resetReactiveLatencyForTest() {
|
|
|
560
579
|
latencyBannerShown = false
|
|
561
580
|
}
|
|
562
581
|
|
|
582
|
+
// --- Global reactive-activity signal (issue #201) --------------------------
|
|
583
|
+
// A DOCUMENT-LEVEL count of in-flight reactive operations — the direct analogue
|
|
584
|
+
// of Turbo's progress bar, but for reactive round trips and deferred renders
|
|
585
|
+
// instead of navigations. Anything that starts an async reactive operation calls
|
|
586
|
+
// enterReactiveActivity(); when it settles (success OR failure, on every path) it
|
|
587
|
+
// calls exitReactiveActivity(). The count is exposed two ways so an app — or a
|
|
588
|
+
// system test — can key off "is the reactive layer settling?" without knowing
|
|
589
|
+
// about any individual root:
|
|
590
|
+
//
|
|
591
|
+
// * a marker on <html>: data-reactive-active present while count > 0 (CSS can
|
|
592
|
+
// drive a global spinner; code/tests can read it). A DISTINCT name from the
|
|
593
|
+
// per-root data-reactive-busy so a [data-reactive-busy] selector never also
|
|
594
|
+
// matches the document element.
|
|
595
|
+
// * events on document: reactive:busy on the 0 -> >0 edge, reactive:idle on the
|
|
596
|
+
// >0 -> 0 edge — EDGES ONLY (not once per op), each carrying { count }.
|
|
597
|
+
//
|
|
598
|
+
// It sums ACROSS all reactive roots (module-level, not per-controller) and across
|
|
599
|
+
// the two async lifecycles wired below:
|
|
600
|
+
// * dispatch — entered in #applyBusy (at ENQUEUE, so the queue wait counts too),
|
|
601
|
+
// exited in the settle closure #perform runs in its finally.
|
|
602
|
+
// * defer — entered/exited with the pendingDefers registry (set/delete), the
|
|
603
|
+
// ONE registry both the fetch (pull) and the stream (push) lane maintain — so
|
|
604
|
+
// the push lane stays balanced even though it clears its pending markers by a
|
|
605
|
+
// node swap, not clearDeferPending. A supersede is delete-then-set (net zero),
|
|
606
|
+
// which is correct: a fast typist's replaced defer is still "layer busy".
|
|
607
|
+
//
|
|
608
|
+
// compute-seed is deliberately NOT counted: recompute() is synchronous, so a seed
|
|
609
|
+
// is fully applied by the time the call returns — there is no async window to await
|
|
610
|
+
// (the "value settles a beat after a morph/seed" case the issue describes is
|
|
611
|
+
// covered by the System test helpers' re-resolve-by-id polling, not this counter).
|
|
612
|
+
export const ACTIVE_ATTR = "data-reactive-active"
|
|
613
|
+
|
|
614
|
+
let activityCount = 0
|
|
615
|
+
|
|
616
|
+
// Increment the global in-flight count; on the 0 -> 1 edge, mark <html> and fire
|
|
617
|
+
// reactive:busy. Fully defensive — a non-browser/test document with no
|
|
618
|
+
// documentElement/dispatchEvent still tracks the count and simply skips the DOM
|
|
619
|
+
// side effects (a global signal must never throw during bootstrap or a round trip).
|
|
620
|
+
export function enterReactiveActivity() {
|
|
621
|
+
activityCount++
|
|
622
|
+
if (activityCount === 1) syncReactiveActivity("reactive:busy")
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Decrement the global in-flight count, clamped at 0 so an unbalanced exit can
|
|
626
|
+
// never drive it negative (which would wedge the marker on forever). On the
|
|
627
|
+
// 1 -> 0 edge, clear the <html> marker and fire reactive:idle.
|
|
628
|
+
export function exitReactiveActivity() {
|
|
629
|
+
if (activityCount === 0) return
|
|
630
|
+
activityCount--
|
|
631
|
+
if (activityCount === 0) syncReactiveActivity("reactive:idle")
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// The current in-flight count — a test seam and a runtime read (an app can gate an
|
|
635
|
+
// "unsaved changes" prompt on `reactiveActivityCount() > 0`).
|
|
636
|
+
export function reactiveActivityCount() {
|
|
637
|
+
return activityCount
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Test seam: reset the module-level counter (and clear the marker) between tests,
|
|
641
|
+
// since the module is imported once per bun run.
|
|
642
|
+
export function resetReactiveActivity() {
|
|
643
|
+
activityCount = 0
|
|
644
|
+
const root = typeof document !== "undefined" ? document.documentElement : null
|
|
645
|
+
root?.removeAttribute?.(ACTIVE_ATTR)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Write the <html> marker from the current count and fire the edge event on
|
|
649
|
+
// document. Both sides are independently guarded so a partial document stub (a
|
|
650
|
+
// documentElement without toggleAttribute, or a document without dispatchEvent)
|
|
651
|
+
// degrades to a no-op rather than throwing.
|
|
652
|
+
function syncReactiveActivity(eventName) {
|
|
653
|
+
if (typeof document === "undefined") return
|
|
654
|
+
const root = document.documentElement
|
|
655
|
+
if (typeof root?.toggleAttribute === "function") {
|
|
656
|
+
root.toggleAttribute(ACTIVE_ATTR, activityCount > 0)
|
|
657
|
+
}
|
|
658
|
+
if (typeof document.dispatchEvent === "function" && typeof CustomEvent === "function") {
|
|
659
|
+
document.dispatchEvent(new CustomEvent(eventName, { detail: { count: activityCount } }))
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
563
663
|
export function registerReactiveActions() {
|
|
564
664
|
registerReactiveVisit()
|
|
565
665
|
registerReactiveToken()
|
|
@@ -1031,6 +1131,9 @@ export default class extends Controller {
|
|
|
1031
1131
|
// Option filtering (issue #163): the ONE delegated sync handler shared by the
|
|
1032
1132
|
// root's input/turbo:morph-element listeners, held for teardown.
|
|
1033
1133
|
#boundSyncFilter
|
|
1134
|
+
// Connect-time compute seed (issue #199): the bound re-seed attached to
|
|
1135
|
+
// turbo:morph-element so an in-place morph re-runs the compute, held for teardown.
|
|
1136
|
+
#boundSeedCompute
|
|
1034
1137
|
// Lazy initial mount (issue #165): the bound re-probe attached to
|
|
1035
1138
|
// turbo:morph-element so a Turbo page-refresh morph re-fires the defer fetch.
|
|
1036
1139
|
#boundProbeLazyDefer
|
|
@@ -1136,6 +1239,32 @@ export default class extends Controller {
|
|
|
1136
1239
|
this.element.addEventListener?.("turbo:morph-element", this.#boundSyncFilter)
|
|
1137
1240
|
this.#syncFilter()
|
|
1138
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
// Connect-time compute seed (issue #199) — ONLY when the root carries a
|
|
1244
|
+
// reactive_compute binding that opts in (data-reactive-compute-seed). A
|
|
1245
|
+
// freshly-rendered compute root (a first paint, or a server validation-error
|
|
1246
|
+
// re-render that replaced the body) computed NOTHING until the first user
|
|
1247
|
+
// `input`; apps worked around it by dispatching a synthetic seed `input` on
|
|
1248
|
+
// connect, but the compute root is a distinct Stimulus controller that may
|
|
1249
|
+
// connect a frame later, so the seed raced its own wiring — the reported
|
|
1250
|
+
// symptom being a PARTIAL apply (an early output paints; a later output + the
|
|
1251
|
+
// mirror stay blank). Running ONE recompute() HERE — after Stimulus has fully
|
|
1252
|
+
// connected the controller and wired the input->recompute delegation — runs
|
|
1253
|
+
// the whole single-pass write set (issue #183) synchronously, so every
|
|
1254
|
+
// declared output, text sink, and cross-root mirror paints from one reducer
|
|
1255
|
+
// result. It is client-only (recompute never enqueues a round trip) and
|
|
1256
|
+
// idempotent (change-guarded writes make a re-seed a no-op — an app still
|
|
1257
|
+
// dispatching a synthetic input is harmless). A plain replace re-connects and
|
|
1258
|
+
// re-seeds; an in-place morph keeps the element CONNECTED and fires no
|
|
1259
|
+
// Stimulus lifecycle, so ALSO re-seed on turbo:morph-element (the show/filter/
|
|
1260
|
+
// dirty precedent). No event is passed, so meta.changed is null — the correct
|
|
1261
|
+
// "no field edited yet" seed semantics; a convergent reducer's default branch
|
|
1262
|
+
// computes the full settled set (see compute.js CONVERGENCE REQUIREMENT).
|
|
1263
|
+
if (this.#computeSeedEnabled()) {
|
|
1264
|
+
this.#boundSeedCompute = () => this.recompute()
|
|
1265
|
+
this.element.addEventListener?.("turbo:morph-element", this.#boundSeedCompute)
|
|
1266
|
+
this.recompute()
|
|
1267
|
+
}
|
|
1139
1268
|
}
|
|
1140
1269
|
|
|
1141
1270
|
// Whether this root opts into dirty tracking (issue #103): track_dirty: puts the
|
|
@@ -1162,6 +1291,7 @@ export default class extends Controller {
|
|
|
1162
1291
|
this.#teardownDirtyTracking()
|
|
1163
1292
|
this.#teardownShowSync()
|
|
1164
1293
|
this.#teardownFilterSync()
|
|
1294
|
+
this.#teardownComputeSeed()
|
|
1165
1295
|
if (this.#boundProbeLazyDefer) {
|
|
1166
1296
|
this.element.removeEventListener?.("turbo:morph-element", this.#boundProbeLazyDefer)
|
|
1167
1297
|
}
|
|
@@ -2720,6 +2850,14 @@ export default class extends Controller {
|
|
|
2720
2850
|
)
|
|
2721
2851
|
}
|
|
2722
2852
|
|
|
2853
|
+
// Whether this root opts into the connect-time compute seed (issue #199).
|
|
2854
|
+
// reactive_compute's root binding emits data-reactive-compute-seed="true"; a
|
|
2855
|
+
// root without a compute binding (or with the seed opted out) pays one
|
|
2856
|
+
// attribute read and never seeds. A quick read, evaluated once per connect.
|
|
2857
|
+
#computeSeedEnabled() {
|
|
2858
|
+
return this.element.getAttribute?.("data-reactive-compute-seed") === "true"
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2723
2861
|
// Whether a delegated input event came from the NAMED filter input (issue
|
|
2724
2862
|
// #163). Anything else — another field's keystroke, a target without
|
|
2725
2863
|
// matches() — skips the filter pass (the morph re-sync path bypasses this).
|
|
@@ -2788,6 +2926,15 @@ export default class extends Controller {
|
|
|
2788
2926
|
this.#boundSyncFilter = undefined
|
|
2789
2927
|
}
|
|
2790
2928
|
|
|
2929
|
+
// Remove the compute seed morph listener on disconnect (issue #199), so a
|
|
2930
|
+
// stray turbo:morph-element after the element leaves the DOM never re-seeds
|
|
2931
|
+
// against a detached root.
|
|
2932
|
+
#teardownComputeSeed() {
|
|
2933
|
+
if (!this.#boundSeedCompute) return
|
|
2934
|
+
this.element.removeEventListener?.("turbo:morph-element", this.#boundSeedCompute)
|
|
2935
|
+
this.#boundSeedCompute = undefined
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2791
2938
|
// Build the multipart body (issue #34). `token`/`act` are flat fields the
|
|
2792
2939
|
// endpoint reads from params[:token]/params[:act]; scalar params nest under
|
|
2793
2940
|
// params[<key>] (Rails parses the bracket into params[:params]); each file is
|
|
@@ -2946,12 +3093,19 @@ export default class extends Controller {
|
|
|
2946
3093
|
#applyBusy(action, trigger, busy) {
|
|
2947
3094
|
this.#markBusy(action, trigger)
|
|
2948
3095
|
const undo = busy ? this.#applyHint(busy, trigger, false) : []
|
|
3096
|
+
// Global activity signal (issue #201): this enqueue is one in-flight reactive
|
|
3097
|
+
// operation. Entered HERE (at enqueue, via #applyBusy) so the document marker
|
|
3098
|
+
// covers the whole pending window — queue wait included — exactly like the
|
|
3099
|
+
// per-root busy vocabulary. Exited once in the settle closure below, which
|
|
3100
|
+
// #perform runs in its finally on EVERY exit path (success or any failure).
|
|
3101
|
+
enterReactiveActivity()
|
|
2949
3102
|
|
|
2950
3103
|
let settled = false
|
|
2951
3104
|
return () => {
|
|
2952
3105
|
if (settled) return // one settle per enqueue, even if called twice
|
|
2953
3106
|
settled = true
|
|
2954
3107
|
this.#unmarkBusy(action, trigger)
|
|
3108
|
+
exitReactiveActivity()
|
|
2955
3109
|
// Busy reverts on SETTLE regardless of outcome, guarded per element.
|
|
2956
3110
|
for (const op of undo) op()
|
|
2957
3111
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Controller as i}from"@hotwired/stimulus";import{confirmResolver as k}from"phlex/reactive/confirm";import{computeReducer as n}from"phlex/reactive/compute";import{confirmPredicate as a}from"phlex/reactive/confirm_predicate";function r(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:visit"])return;j["reactive:visit"]=function(){let Q=this.getAttribute("data-url");if(Q)window.Turbo.visit(Q,{action:"advance"})}}function t(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:token"])return;j["reactive:token"]=function(){let Q=this.getAttribute("data-reactive-token-value"),X=this.getAttribute("target");if(!Q||!X)return;let Z=document.getElementById(X);if(Z)Z.setAttribute("data-reactive-token-value",Q)}}function e(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:js"])return;j["reactive:js"]=function(){let Q=o(this.getAttribute("data-reactive-ops"));if(!Q.length)return;let X=this.getAttribute("target"),Z=X?document.getElementById(X):null;if(X&&!Z)return;s(Q,($)=>Fj($,Z))}}var N=new Map;function Sj(){N.clear(),R=!1}function kj(j){return N.get(j)?.via}var R=!1;function jj(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:defer"])return;if(j["reactive:defer"]=function(){let Q=this.getAttribute("target");if(!Q)return;if(this.getAttribute("data-reactive-defer-via")==="stream"){Xj(Q,this);return}let X=this.getAttribute("data-reactive-defer-token");if(!X)return;B(Q,X)},!R&&typeof document<"u"&&document.addEventListener)R=!0,document.addEventListener("turbo:before-stream-render",Qj)}function Qj(j){let X=j.target?.getAttribute?.("target");if(!X)return;let Z=X.startsWith("reactive-defer-src-")?X.slice(19):X;if(N.get(Z)?.via==="stream")N.delete(Z)}function B(j,Q){let X=document.getElementById(j);if(!X){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}f(j),u(X);let Z={via:"fetch",abort:new AbortController,timedOut:!1};N.set(j,Z),Zj(j,Z,Q)}function Xj(j,Q){let X=document.getElementById(j);if(!X){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}let Z=Q.getAttribute("data-reactive-defer-src");if(!Z)return;if(!globalThis.customElements?.get?.("pgbus-stream-source")){let J=Q.getAttribute("data-reactive-defer-token");if(J){B(j,J);return}console.error("[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered "+"and no fallback token was provided — is the pgbus client loaded on this page?");return}f(j),u(X);let $=document.createElement("pgbus-stream-source");$.id=h(j),$.setAttribute("src",Z),$.setAttribute("since-id",Q.getAttribute("data-reactive-defer-since-id")??"0"),$.setAttribute("hidden",""),document.body.appendChild($),N.set(j,{via:"stream"})}function h(j){return`reactive-defer-src-${j}`}async function Zj(j,Q,X){let Z=setTimeout(()=>{Q.timedOut=!0,Q.abort.abort()},Gj()),$;try{$=await fetch($j(),{method:"POST",headers:{Accept:"text/vnd.turbo-stream.html","Content-Type":"application/json","X-CSRF-Token":Jj()},body:JSON.stringify({token:X}),credentials:"same-origin",signal:Q.abort.signal})}catch(G){if(clearTimeout(Z),N.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed",G),P(j,X);return}if(N.get(j)!==Q){clearTimeout(Z);return}if($.status===204){clearTimeout(Z),w(j);return}if(!$.ok){clearTimeout(Z),console.error(`[phlex-reactive] deferred render failed: HTTP ${$.status}`),P(j,X,$.status);return}let J;try{J=await $.text()}catch(G){if(clearTimeout(Z),N.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed reading the body",G),P(j,X);return}if(clearTimeout(Z),N.get(j)!==Q)return;w(j),window.Turbo.renderStreamMessage(J)}function f(j){let Q=N.get(j);if(!Q)return;if(N.delete(j),Q.via==="fetch")Q.abort.abort();else document.getElementById(h(j))?.remove?.()}function u(j){j.setAttribute("data-reactive-defer-pending","true"),j.setAttribute("aria-busy","true")}function p(j){j.removeAttribute("data-reactive-defer-pending"),j.removeAttribute("aria-busy")}function w(j){N.delete(j);let Q=document.getElementById(j);if(!Q)return;p(Q),Q.removeAttribute("data-reactive-error")}function P(j,Q,X){N.delete(j);let Z=document.getElementById(j);if(!Z)return;p(Z),Z.setAttribute("data-reactive-error","defer");let $=()=>{let J=document.getElementById(j);if(!J){console.warn("[phlex-reactive] defer retry() ignored — the target left the DOM");return}J.removeAttribute("data-reactive-error"),B(j,Q)};Z.dispatchEvent(new CustomEvent("reactive:error",{bubbles:!0,composed:!0,detail:{kind:"defer",target:j,status:X,retry:$}}))}function $j(){return document.querySelector('meta[name="phlex-reactive-defer-path"]')?.content||"/reactive/defer"}function Jj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}function Gj(){let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:30000}var T=!1;function zj(){if(T)return;if(typeof document>"u"||!document.addEventListener)return;T=!0,document.addEventListener("turbo:before-stream-render",Kj)}function Kj(j){let Q=j.detail,X=Q?.render;if(typeof X!=="function"||X.__reactiveDismissWrapped){if(typeof requestAnimationFrame==="function")requestAnimationFrame(C);else setTimeout(C,0);return}let Z=async($)=>{await X($),C()};Z.__reactiveDismissWrapped=!0,Q.render=Z}function C(){let j=document.querySelectorAll("[data-reactive-dismiss-after]");for(let Q of j){if(Q.hasAttribute("data-reactive-dismiss-scheduled"))continue;let X=Number(Q.getAttribute("data-reactive-dismiss-after"));if(!Number.isFinite(X)||X<=0)continue;Q.setAttribute("data-reactive-dismiss-scheduled",""),setTimeout(()=>Q.remove(),X)}}function wj(){T=!1}var D=!1;function qj(){if(D)return;if(typeof window>"u"||typeof document>"u")return;if(typeof window.addEventListener!=="function")return;D=!0;let j=()=>{let Q=document.documentElement;if(typeof Q?.toggleAttribute!=="function")return;Q.toggleAttribute("data-reactive-offline",globalThis.navigator?.onLine===!1)};j(),window.addEventListener("online",j),window.addEventListener("offline",j)}function bj(){D=!1}var I="phlex-reactive:latency",x=!1;function Yj(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(I,String(j))}function Uj(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(I),x=!1}function Hj(){if(typeof window>"u"||typeof document>"u")return;if(document.querySelector?.('meta[name="phlex-reactive-env"]')?.content!=="development")return;window.PhlexReactive={enableLatencySim:Yj,disableLatencySim:Uj}}function vj(){x=!1}function b(){r(),t(),e(),jj(),zj(),qj(),Hj()}function Wj(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)b();else document.addEventListener("turbo:load",b,{once:!0});var O=!1;function Aj(){if(O)return;if(typeof document>"u")return;let j=document.querySelectorAll('[data-controller~="reactive"]');if(!j||j.length===0)return;console.warn("[phlex-reactive] found "+j.length+' element(s) with data-controller="reactive" '+"but the reactive controller never connected. It is loaded but not registered — "+'add `application.register("reactive", ReactiveController)` (importmap) or import it into app/javascript/controllers/ for lazyLoadControllersFrom apps. See the README.')}function yj(){O=!1}function hj(){O=!0}if(typeof window<"u"&&typeof document<"u"){let j=()=>setTimeout(Aj,0);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",j,{once:!0});else j()}var _j=new Set(["href","src","srcdoc","action","formaction","xlink:href","style"]);function Lj(j){let Q=String(j).toLowerCase();return Q.startsWith("on")||_j.has(Q)}function Nj(j,Q,X){let[Z,$,J]=Q;j.classList.add(Z,$),X(),requestAnimationFrame(()=>{j.classList.remove($),j.classList.add(J)});let G=!1,K=()=>{if(G)return;G=!0,j.classList.remove(Z,J)};j.addEventListener("animationend",K,{once:!0}),setTimeout(K,350)}var v=Object.freeze({show:(j,Q)=>E(j,!1,Q),hide:(j,Q)=>E(j,!0,Q),toggle:(j,Q)=>E(j,!j.hidden,Q),add_class:(j,Q)=>j.classList.add(...Q.classes??[]),remove_class:(j,Q)=>j.classList.remove(...Q.classes??[]),toggle_class:(j,Q)=>(Q.classes??[]).forEach((X)=>j.classList.toggle(X)),set_attr:(j,Q)=>{if(F(Q.name))j.setAttribute(Q.name,Q.value??"")},remove_attr:(j,Q)=>{if(F(Q.name))j.removeAttribute(Q.name)},toggle_attr:(j,Q)=>{if(!F(Q.name))return;if(j.hasAttribute(Q.name))j.removeAttribute(Q.name);else j.setAttribute(Q.name,"")},focus:(j)=>j.focus?.(),focus_first:(j)=>Ej(j)?.focus?.(),text:(j,Q)=>{let X=String(Q.value??"");if(j.textContent!==X)j.textContent=X},dispatch:(j,Q)=>{j.dispatchEvent(new CustomEvent(Q.name,{bubbles:!0,composed:!0,detail:Q.detail??{}}))}});function E(j,Q,X){if(X?.transition)Nj(j,X.transition,()=>j.hidden=Q);else j.hidden=Q}function F(j){if(!Lj(j))return!0;return console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(j)} — skipped`),!1}var m=/^#[A-Za-z_][\w-]*$/;function Vj(j){if(typeof j==="string"&&m.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(j)} — skipped`),!1}function Mj(j,Q){let X=j.getAttribute("data-reactive-show-equals");if(X!==null)return Q===X;let Z=j.getAttribute("data-reactive-show-not");if(Z!==null)return Q!==Z;let $=j.getAttribute("data-reactive-show-in");if($!==null){try{let J=JSON.parse($);if(Array.isArray(J))return J.includes(Q)}catch{}return console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify($)} — skipped`),null}for(let J of g){let G=j.getAttribute(`data-reactive-show-${J}`);if(G!==null)return c(J,G,Q)}return console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped"),null}var g=["gte","gt","lte","lt"];function c(j,Q,X){let Z=Number(Q);if(Number.isNaN(Z))return console.warn(`[phlex-reactive] reactive_show ${j}: needs a numeric literal, got ${JSON.stringify(Q)} — skipped`),null;let $=X==null?"":String(X).trim(),J=$===""?NaN:Number($);if(Number.isNaN(J))return!1;switch(j){case"gte":return J>=Z;case"gt":return J>Z;case"lte":return J<=Z;case"lt":return J<Z;default:return null}}function d(j,Q){if(!j||typeof j!=="object")return null;if(typeof j.equals==="string")return Q===j.equals;if(typeof j.not==="string")return Q!==j.not;if(Array.isArray(j.in))return j.in.includes(Q);for(let X of g)if(X in j)return c(X,j[X],Q);return null}var y="[data-reactive-show-field], [data-reactive-show]";function xj(j){try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn(`[phlex-reactive] malformed compound reactive_show payload ${JSON.stringify(j)} — skipped`),null}function S(j,Q){if(!j||typeof j!=="object"||typeof j.field!=="string")return!1;let X=Q(j.field)??"";return d(j,X)===!0}function l(j,Q){if(!Array.isArray(j)||j.length===0)return null;return j.some((X)=>Array.isArray(X)&&X.length>0&&X.every((Z)=>S(Z,Q)))}function Bj(j,Q){if(!j||typeof j!=="object")return null;let X=j.any;if(Array.isArray(X)&&(X.length===0||Array.isArray(X[0])))return l(X,Q);return Oj(j,Q)}function Oj(j,Q){let X=Array.isArray(j.all)?"all":Array.isArray(j.any)?"any":null;if(!X)return null;let Z=j[X];if(Z.length===0)return null;let $=Z.map((J)=>S(J,Q));return X==="all"?$.every(Boolean):$.some(Boolean)}function Pj(j){if(typeof j==="string"&&m.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root show target ${JSON.stringify(j)} — skipped`),!1}var Cj='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function Ej(j){return j.querySelectorAll?.(Cj)?.[0]??null}function o(j){if(Array.isArray(j))return j;if(typeof j!=="string")return[];try{let Q=JSON.parse(j);return Array.isArray(Q)?Q:[]}catch{return[]}}function s(j,Q){for(let X of j){if(!Array.isArray(X))continue;let[Z,$={}]=X;if(!Object.hasOwn(v,Z)){console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(Z)} — skipped`);continue}for(let J of Q($))v[Z](J,$)}}function Fj(j,Q){let X=j.to;if(Q){if(X==="@root")return[Q];if(typeof X!=="string"||X==="")return[];if(j.global)return[...document.querySelectorAll(X)];return[...Q.querySelectorAll(X)]}if(typeof X!=="string"||X===""||X==="@root")return[];return[...document.querySelectorAll(X)]}class fj extends i{static values={token:String};#F;#A=new Map;#K=new Map;#t;#M;#R;#x=0;#q=new Map;#B=new WeakMap;#_=new Map;#T=new WeakSet;#Y;#U;#H;#j;#X;#L;connect(){if(O=!0,this.element.id==="")console.warn("[phlex-reactive] a reactive root has no id; its next-action token can't self-match "+"and may fall back to the first token in the response → a silent HTTP 403 on the NEXT action. "+"Put id: on the SAME element as reactive_attrs — use div(**reactive_root) (emits id + token together), "+"or div(id:, **reactive_attrs). The id: must NOT be on a child. See the README.");if(this.element.getAttribute?.("data-reactive-defer-token"))this.#D(),this.#L=()=>this.#D(),this.element.addEventListener?.("turbo:morph-element",this.#L);if(this.#e()){if(this.#Y=()=>this.#C(),this.element.addEventListener?.("turbo:morph-element",this.#Y),this.#C(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#Mj()}if(this.#Bj())this.#j=()=>this.#m(),this.element.addEventListener?.("input",this.#j),this.element.addEventListener?.("change",this.#j),this.element.addEventListener?.("turbo:morph-element",this.#j),this.#m();if(this.#Ej())this.#X=(j)=>{if(j?.type==="input"&&!this.#Fj(j))return;this.#d()},this.element.addEventListener?.("input",this.#X),this.element.addEventListener?.("turbo:morph-element",this.#X),this.#d()}#e(){if((this.element.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let j=this.element.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}disconnect(){if(this.#Kj(),this.#Yj(),this.#xj(),this.#Cj(),this.#Rj(),this.#L)this.element.removeEventListener?.("turbo:morph-element",this.#L)}#D(){let j=this.element;if(!j?.id)return;let Q=j.getAttribute?.("data-reactive-defer-token");if(!Q)return;if(j.getAttribute?.("data-reactive-defer-pending")!=="true")return;B(j.id,Q)}dispatch(j){let{action:Q,params:X,debounce:Z,throttle:$,confirm:J,confirmWhen:G,outside:K,window:z,optimistic:Y}=j.params;if(!Q)return;let H=j.params.busy??this.#hj(j.params.loading);if(K&&this.element.contains(j.target))return;let _=j.currentTarget??j.target;if(!z&&!this.#Ij(Y,_))j.preventDefault();let W=this.#f(J,G);if(!W)return this.#w(_,Q,X,Z,$,Y,H);Promise.resolve().then(()=>k(W)).catch(()=>!1).then((A)=>{if(A)this.#w(_,Q,X,Z,$,Y,H)})}runOps(j){let{ops:Q,confirm:X,confirmWhen:Z,outside:$,window:J}=j.params;if($&&this.element.contains(j.target))return;if(!J)j.preventDefault();let G=this.#f(X,Z);if(!G)return this.#s(this.#o(Q));Promise.resolve().then(()=>k(G)).catch(()=>!1).then((K)=>{if(K)this.#s(this.#o(Q))})}trackDirty(){this.#C()}recompute(j){if(j&&this.#T.has(j))return;let Q=this.#Qj(),X=Q.map(([q])=>q),Z=this.element.getAttribute?.("data-reactive-scope")||null,$=(q)=>Z&&!q.includes("[")?`${Z}[${q}]`:q,J=this.#W(),G=new Map,K=(q)=>{if(G.has(q))return G.get(q);let U=null;for(let V of this.element.querySelectorAll(`[name="${$(q)}"]`))if(J(V)){U=V;break}return G.set(q,U),U};for(let q of X)this.#S(q,K(q)?.value??"");let z=this.element.getAttribute("data-reactive-compute-reducer-param"),Y=z?n(z):null;if(!Y){this.#k({},K);return}let H=this.#jj("data-reactive-compute-outputs-param"),_={};for(let[q,U]of Q){let V=K(q);if(U==="string")_[q]=V?.value??"";else{let L=Number(V?.value);_[q]=Number.isFinite(L)?L:0}}let W=Y(_,{changed:this.#Xj(j,X,Z)})||{},A=[];for(let q of H){if(!(q in W))continue;let U=K(q);if(!U)continue;if(String(W[q])===U.value)continue;U.value=W[q],A.push(U)}for(let q of Object.keys(W)){let U=W[q];if(U===void 0||U===null)continue;this.#S(q,U)}this.#k(W,K);for(let q of A){let U=new Event("input",{bubbles:!0});this.#T.add(U),q.dispatchEvent(U)}}listnavNext(j){this.#I(j,1)}listnavPrev(j){this.#I(j,-1)}listnavPick(j){let Q=this.#O(j),X=Q.findIndex((Z)=>Z.hasAttribute("data-reactive-highlighted"));if(X<0)return;j.preventDefault(),Q[X].click()}listnavClose(j){for(let Q of this.#O(j))Q.removeAttribute("data-reactive-highlighted")}#I(j,Q){let X=this.#O(j);if(!X.length)return;j.preventDefault();let Z=X.findIndex((G)=>G.hasAttribute("data-reactive-highlighted")),$=Z<0?Q>0?0:X.length-1:(Z+Q+X.length)%X.length;for(let G of X)G.removeAttribute("data-reactive-highlighted");let J=X[$];J.setAttribute("data-reactive-highlighted","true"),J.scrollIntoView?.({block:"nearest"})}#O(j){let X=(j?.currentTarget??j?.target??this.element).getAttribute?.("data-reactive-listnav-option-param")??this.element.getAttribute("data-reactive-listnav-option-param");if(!X)return[];let Z=this.#W();return Array.from(this.element.querySelectorAll(X)).filter(($)=>!$.hidden&&Z($))}#jj(j){let Q=this.element.getAttribute(j);if(!Q)return[];try{let X=JSON.parse(Q);return Array.isArray(X)?X:[]}catch{return[]}}#Qj(){let j=this.element.getAttribute("data-reactive-compute-inputs-param");if(!j)return[];try{let Q=JSON.parse(j);if(Array.isArray(Q))return Q.map((X)=>[X,"number"]);if(Q&&typeof Q==="object")return Object.entries(Q);return[]}catch{return[]}}#Xj(j,Q,X){let Z=j?.target;if(!Z?.name||typeof Z.closest!=="function")return null;let $=this.#Zj(Z.name,X);if(!Q.includes($))return null;return this.#Q(Z)?$:null}#Zj(j,Q){if(!Q)return j;let X=`${Q}[`;return j.startsWith(X)&&j.endsWith("]")?j.slice(X.length,-1):j}#S(j,Q){let X=String(Q);for(let Z of this.#$j(j)){if(Z.textContent===X)continue;Z.textContent=X}}#$j(j){let Q=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(Q).filter((X)=>this.#Q(X))}#k(j,Q){let X=this.#Jj();for(let[Z,$]of Object.entries(X)){let J=Z in j?j[Z]:Q(Z)?.value;if(J===void 0||J===null)continue;let G=String(J);for(let K of Array.isArray($)?$:[$]){if(!Vj(K))continue;for(let z of document.querySelectorAll(K)){if(z.textContent===G)continue;z.textContent=G}}}}#Jj(){let j=this.element.getAttribute("data-reactive-compute-mirror-param");if(!j)return{};try{let Q=JSON.parse(j);return Q&&typeof Q==="object"&&!Array.isArray(Q)?Q:{}}catch{return{}}}#w(j,Q,X,Z,$,J,G){if(this.#V("reactive:before-dispatch",{action:Q,params:this.#l(X),element:this.element},{cancelable:!0}).defaultPrevented)return;let z=Number(Z)||0;if(z>0)return this.#zj(j,z,Q,X,J,G);let Y=Number($)||0;if(Y>0)return this.#qj(j,Y,Q,X,J,G);return this.#N(Q,X,J,j,G)}#N(j,Q,X,Z,$){let J=this.#Sj(X,Z),G=this.#kj(j,Z,$),K=this.#b()?this.#Gj(X,Z):null;return this.queue=(this.queue??Promise.resolve()).then(()=>this.#Lj(j,Q,J,G,K)),this.queue}#Gj(j,Q){if(!j?.hide)return null;let X=this.#a(j,Q);if(!X.length)return null;return()=>{let Z=X.filter(($)=>$.isConnected&&!$.hidden);if(!Z.length)return;console.warn("[phlex-reactive] optimistic: { hide: true } was undone by the reply's re-render — "+"the element is visible again. For an instant delete, return reply.remove so the server removes it; otherwise the hide only flashes.",Z)}}#zj(j,Q,X,Z,$,J){this.#P(j);let G=()=>{this.#P(j),this.#N(X,Z,$,j,J)},K=setTimeout(G,Q);j?.addEventListener?.("blur",G,{once:!0}),this.#A.set(j,{timer:K,flush:G})}#P(j){let Q=this.#A.get(j);if(!Q)return;clearTimeout(Q.timer),j?.removeEventListener?.("blur",Q.flush),this.#A.delete(j)}#Kj(){for(let j of[...this.#A.keys()])this.#P(j)}#qj(j,Q,X,Z,$,J){let G=this.#K.get(j)??new Map;if(G.has(X))return;let K=setTimeout(()=>{if(G.delete(X),G.size===0)this.#K.delete(j)},Q);return G.set(X,K),this.#K.set(j,G),this.#N(X,Z,$,j,J)}#Yj(){for(let j of this.#K.values())for(let Q of j.values())clearTimeout(Q);this.#K.clear()}#V(j,Q,{cancelable:X=!1}={}){let Z=new CustomEvent(j,{bubbles:!0,composed:!0,cancelable:X,detail:Q});return(this.element.isConnected?this.element:document).dispatchEvent(Z),Z}#Z(j,Q,X,Z){let $=()=>{if(!this.element.isConnected){console.warn("[phlex-reactive] retry() ignored — the reactive root left the DOM");return}return this.#N(j,Q)};this.#V("reactive:error",{action:j,params:X,...Z,retry:$})}#$(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#Uj(){this.element?.removeAttribute?.("data-reactive-error")}#Hj(){let j=document.querySelector("[data-reactive-error-flash]");if(!j?.content)return;let Q=j.getAttribute("data-reactive-error-flash")||"flash",X=document.getElementById(Q);if(!X)return;X.appendChild(j.content.cloneNode(!0))}#Wj(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(I));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!x)x=!0,console.warn(`[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${j}ms. Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.`);return new Promise((Q)=>setTimeout(Q,j))}#b(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#v(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#Aj(j){if(!j)return[];let Q=[],X=/<turbo-stream\b([^>]*)>/g,Z;while((Z=X.exec(j))!==null){let $=Z[1],J=$.match(/\baction="([^"]*)"/)?.[1]??"?",G=$.match(/\btarget="([^"]*)"/)?.[1];Q.push(G?`${J} → #${G}`:J)}return Q}#_j(j){let{action:Q,status:X,ms:Z}=j,J=`reactive ${this.element?.id?`#${this.element.id} `:""}${Q} → ${X??"—"} (${Math.round(Z)}ms)`;if(console.groupCollapsed(J),console.log(`params: [${j.paramNames.join(", ")}] + collected: [${j.fieldNames.join(", ")}]`),console.log(`encoding: ${j.encoding}`),j.streams.length)console.log(`streams: ${j.streams.join(" ")}`);console.log(`token: ${j.tokenRefreshed?"refreshed ✓":"unchanged"}`),console.groupEnd()}async#Lj(j,Q,X,Z,$){let{fields:J,files:G}=this.#u(),K=this.#l(Q),z={...J,...K},Y=this.#J,H=G.length>0,_=H?this.#Tj(Y,j,z,G):JSON.stringify({token:Y,act:j,params:z}),W=this.#b()?{action:j,paramNames:Object.keys(K),fieldNames:Object.keys(J),encoding:H?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#v()}:null;await this.#Wj();try{if(navigator.onLine===!1){this.#G(X),this.#$("offline"),this.#Z(j,Q,z,{kind:"offline"});return}let A;try{let L={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#pj()};if(!H)L["Content-Type"]="application/json";let M=this.#mj();if(M)L["X-Pgbus-Connection"]=M;A=await fetch(this.#fj(),{method:"POST",headers:L,body:_,credentials:"same-origin",signal:AbortSignal.timeout(this.#uj())})}catch(L){if(console.error("[phlex-reactive] action error",L),this.#G(X),L?.name==="TimeoutError"||L?.name==="AbortError"){this.#$("timeout"),this.#Z(j,Q,z,{kind:"timeout"});return}this.#Hj(),this.#$("network"),this.#Z(j,Q,z,{kind:"network"});return}if(W)W.status=A.status;if(A.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#G(X),this.#$("redirected"),this.#Z(j,Q,z,{kind:"redirected",status:A.status});return}if(!A.ok){let L=await A.text();if(console.error(`[phlex-reactive] action failed: HTTP ${A.status}`,L),this.#G(X),(A.headers.get("Content-Type")||"").includes("turbo-stream")){let M=this.#h(L);if(this.#J=M??this.#J,W)this.#y(W,L,M);window.Turbo.renderStreamMessage(L)}this.#$("http"),this.#Z(j,Q,z,{kind:"http",status:A.status,body:L});return}let q=A.headers.get("Content-Type")||"";if(!q.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${q}" — no update applied`),this.#G(X),this.#$("content-type"),this.#Z(j,Q,z,{kind:"content-type",status:A.status});return}let U=await A.text(),V=this.#h(U);if(this.#J=V??this.#J,W)this.#y(W,U,V);if(window.Turbo.renderStreamMessage(U),$)queueMicrotask($);this.#Uj(),this.#V("reactive:applied",{action:j,params:z,html:U})}catch(A){console.error("[phlex-reactive] action error",A),this.#G(X),this.#V("reactive:error",{action:j,params:z,kind:"apply"})}finally{if(Z?.(),W)this.#_j({...W,ms:this.#v()-W.started})}}#y(j,Q,X){j.streams=this.#Aj(Q),j.tokenRefreshed=X!=null}get#J(){return this.#F??this.tokenValue}set#J(j){this.#F=j}#h(j){let Q=this.element.id;if(!Q)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:X,self:Z}=this.#Nj(Q),$=j.match(X);if($)return $[1];let J=j.match(Z);if(J)return J[1].match(/data-reactive-token-value="([^"]+)"/)?.[1];return}#Nj(j){let Q=this.#R;if(Q&&Q.id===j)return Q;let X=Wj(j);return this.#R={id:j,token:new RegExp(`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${X}"[^>]*\\bdata-reactive-token-value="([^"]+)"`),self:new RegExp(`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${X}"[^>]*>([\\s\\S]*?)</turbo-stream>`)}}#Q(j){return j.closest('[data-controller~="reactive"]')===this.element}#W(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(Q)=>this.#Q(Q)}#f(j,Q){if(j)return j;if(!Q)return null;let X=Q;if(typeof Q==="string")try{X=JSON.parse(Q)}catch{return console.warn(`[phlex-reactive] malformed conditional confirm payload ${JSON.stringify(Q)} — skipped`),null}if(!X||typeof X!=="object")return null;let{fields:Z}=this.#u(),$=(G)=>Z[G],J;if(typeof X.predicate==="string"){let G=a(X.predicate);if(!G)return console.warn(`[phlex-reactive] confirm predicate "${X.predicate}" is not registered — proceeding without a dialog (register it with setConfirmPredicate)`),null;J=!!G(Z)}else J=l(X.groups?.any,$)===!0;return J?X.message:null}#u(){let j={},Q=[],X=this.#W();return this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Z)=>{if(!X(Z))return;if(Z.type==="file")for(let $ of Z.files??[])Q.push({name:Z.name,file:$,multiple:Z.multiple});else if(Z.type==="checkbox")j[Z.name]=Z.checked;else if(Z.type==="radio"){if(Z.checked)j[Z.name]=Z.value}else j[Z.name]=Z.value}),this.element.querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])").forEach((Z)=>{if(!X(Z))return;let $=Z.getAttribute("name");if(!$)return;let J=j[$];if(J==null||J==="")j[$]=Z.value??Z.textContent??Z.innerHTML??""}),{fields:j,files:Q}}#C(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Q)=>{if(!this.#Q(Q))return;if(Q.type==="file")return;if(this.#Vj(Q))Q.setAttribute("data-reactive-dirty","true"),j++;else Q.removeAttribute("data-reactive-dirty")}),j>0)this.element.setAttribute("data-reactive-dirty",String(j));else this.element.removeAttribute("data-reactive-dirty")}#Vj(j){if(j.type==="checkbox"||j.type==="radio")return j.checked!==j.defaultChecked;if(j.tag==="select"||j.options)return Array.from(j.options??[]).some((Q)=>Q.selected!==Q.defaultSelected);return j.value!==j.defaultValue}#p(){let j=this.element.getAttribute?.("data-reactive-dirty"),Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:0}#Mj(){if(typeof window>"u"||typeof window.addEventListener!=="function")return;this.#U=(j)=>{if(this.#p()===0)return;return j.preventDefault(),j.returnValue="You have unsaved changes.",j.returnValue},this.#H=(j)=>{if(this.#p()===0)return;if(!(typeof window.confirm==="function"?window.confirm("You have unsaved changes. Leave anyway?"):!0))j.preventDefault?.()},window.addEventListener("beforeunload",this.#U),window.addEventListener("turbo:before-visit",this.#H)}#xj(){if(this.#Y)this.element.removeEventListener?.("turbo:morph-element",this.#Y),this.#Y=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#U)window.removeEventListener("beforeunload",this.#U);if(this.#H)window.removeEventListener("turbo:before-visit",this.#H)}this.#U=void 0,this.#H=void 0}#Bj(){if(this.element.getAttribute?.("data-reactive-show-targets"))return!0;let j=this.element.querySelectorAll?.(y)??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}#m(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.#W(),Q=this.element.getAttribute?.("data-reactive-scope")||null,X=new Map,Z=($)=>{if(!X.has($))X.set($,this.#c($,j,Q));return X.get($)};for(let $ of this.element.querySelectorAll(y)){if(!j($))continue;let J=$.getAttribute("data-reactive-show");if(J!==null){let Y=Bj(xj(J),Z);if(Y!==null)this.#g($,Y,j,Q);continue}let G=$.getAttribute("data-reactive-show-field");if(!G)continue;let K=Z(G);if(K===null)continue;let z=Mj($,K);if(z===null)continue;this.#g($,z,j,Q)}this.#Oj(j,X,Q)}#g(j,Q,X,Z){if(j.hidden=!Q,j.getAttribute("data-reactive-show-disable")!=="true")return;if(typeof j.querySelectorAll!=="function")return;for(let $ of j.querySelectorAll("input[name], select[name], textarea[name]"))if(X($))$.disabled=!Q;if(j.name&&X(j))j.disabled=!Q}#Oj(j,Q,X){let Z=this.#Pj();for(let[$,J]of Object.entries(Z)){if(!J||typeof J!=="object"||Array.isArray(J))continue;if(!Q.has($))Q.set($,this.#c($,j,X));let G=Q.get($);if(G===null)continue;let K=()=>G;for(let[z,Y]of Object.entries(J)){if(!Pj(z))continue;let H;if(Array.isArray(Y)){if(Y.length===0){console.warn(`[phlex-reactive] malformed reactive_show_targets group for ${z} — skipped`);continue}H=Y.every((_)=>S(_,K))}else{let _=d(Y,G);if(_===null){console.warn(`[phlex-reactive] malformed reactive_show_targets predicate for ${z} — skipped`);continue}H=_}for(let _ of document.querySelectorAll(z))_.hidden=!H}}}#Pj(){let j=this.element.getAttribute?.("data-reactive-show-targets");if(!j)return{};try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn("[phlex-reactive] malformed data-reactive-show-targets — ignored. "+"Did two reactive_show_targets calls collide on one root? Declare every field in ONE call: reactive_show_targets(mode: { ... }, kind: { ... })"),{}}#c(j,Q,X){let Z=X&&!j.includes("[")?`${X}[${j}]`:j,$=!1,J=null;for(let G of this.element.querySelectorAll(`[name="${Z}"]`)){if(!Q(G))continue;if(G.type==="checkbox")return G.checked?"true":"false";if(G.type==="radio"){if(G.checked)return G.value??"";$=!0;continue}J??=G}if(J)return J.value??"";return $?"":null}#Cj(){if(!this.#j)return;this.element.removeEventListener?.("input",this.#j),this.element.removeEventListener?.("change",this.#j),this.element.removeEventListener?.("turbo:morph-element",this.#j),this.#j=void 0}#Ej(){return!!(this.element.getAttribute?.("data-reactive-filter-input")&&this.element.getAttribute?.("data-reactive-filter-option"))}#Fj(j){let Q=this.element.getAttribute("data-reactive-filter-input");return!!Q&&typeof j.target?.matches==="function"&&j.target.matches(Q)}#d(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.element.getAttribute("data-reactive-filter-input"),Q=this.element.getAttribute("data-reactive-filter-option");if(!j||!Q)return;let X=this.#W(),Z=[...this.element.querySelectorAll(j)].find(X);if(!Z)return;let $=(Z.value??"").trim().toLowerCase(),J=0;for(let z of this.element.querySelectorAll(Q)){if(!X(z))continue;let Y=(z.getAttribute("data-reactive-filter-text")??z.textContent??"").toLowerCase(),H=$!==""&&!Y.includes($);if(z.hidden=H,H)z.removeAttribute("data-reactive-highlighted");else J++}let G=this.element.getAttribute("data-reactive-filter-group");if(G)for(let z of this.element.querySelectorAll(G)){if(!X(z))continue;let Y=[...z.querySelectorAll(Q)].filter(X);if(Y.length===0)continue;z.hidden=Y.every((H)=>H.hidden)}let K=this.element.getAttribute("data-reactive-filter-empty");if(K){for(let z of this.element.querySelectorAll(K))if(X(z))z.hidden=J>0}}#Rj(){if(!this.#X)return;this.element.removeEventListener?.("input",this.#X),this.element.removeEventListener?.("turbo:morph-element",this.#X),this.#X=void 0}#Tj(j,Q,X,Z){let $=new FormData;$.append("token",j),$.append("act",Q);for(let[G,K]of Object.entries(X))this.#E($,`params[${G}]`,K);let J=this.#Dj(Z);for(let{name:G,file:K,multiple:z}of Z){let H=z||J.has(G)?`params[${G}][]`:`params[${G}]`;$.append(H,K,K.name)}return $}#E(j,Q,X){if(X==null)j.append(Q,"");else if(Array.isArray(X))X.forEach((Z,$)=>this.#E(j,`${Q}[${$}]`,Z));else if(typeof X==="object")for(let[Z,$]of Object.entries(X))this.#E(j,`${Q}[${Z}]`,$);else j.append(Q,String(X))}#Dj(j){let Q=new Map;for(let{name:X}of j)Q.set(X,(Q.get(X)??0)+1);return new Set([...Q].filter(([,X])=>X>1).map(([X])=>X))}#l(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#o(j){return o(j)}#s(j){s(j,(Q)=>this.#i(Q))}#i(j){let Q=j.to;if(Q==="@root")return[this.element];if(typeof Q!=="string"||Q==="")return[];if(j.global)return[...document.querySelectorAll(Q)];return[...this.element.querySelectorAll(Q)].filter((X)=>this.#Q(X))}#Ij(j,Q){if(j?.checked!=="keep")return!1;let X=Q?.type;return X==="checkbox"||X==="radio"}#Sj(j,Q){if(!j)return null;let X=this.#n(j,Q,!0);return X.length?X:null}#G(j){if(!j)return;if(!this.element.isConnected)return;for(let Q of j)Q()}#kj(j,Q,X){this.#bj(j,Q);let Z=X?this.#n(X,Q,!1):[],$=!1;return()=>{if($)return;$=!0,this.#vj(j,Q);for(let J of Z)J()}}#n(j,Q,X){let Z=[];for(let $ of this.#a(j,Q)){if(j.add_class){let J=j.add_class.filter((G)=>!$.classList.contains(G));if($.classList.add(...J),J.length)Z.push(()=>$.classList.remove(...J))}if(j.remove_class){let J=j.remove_class.filter((G)=>$.classList.contains(G));if($.classList.remove(...J),J.length)Z.push(()=>$.classList.add(...J))}if(j.toggle_class)j.toggle_class.forEach((J)=>$.classList.toggle(J)),Z.push(()=>j.toggle_class.forEach((J)=>$.classList.toggle(J)));if(j.hide)$.hidden=!0,Z.push(()=>$.hidden=!1);if(j.show)$.hidden=!1,Z.push(()=>$.hidden=!0)}if(Q&&(j.disable||j.text!=null))Z.push(this.#wj(j,Q));if(X&&j.checked==="keep"&&Q&&"checked"in Q){let $=Q.checked;Z.push(()=>Q.checked=!$)}return Z}#a(j,Q){if(j.to==null)return Q?[Q]:[];return this.#i({to:j.to})}#wj(j,Q){let X=this.#_.get(Q);if(X)X.count++;else this.#_.set(Q,{count:1,disabled:Q.disabled,html:Q.innerHTML,hadText:j.text!=null,swappedTo:j.text});if(j.disable)Q.disabled=!0;if(j.text!=null)Q.innerHTML=j.text;return()=>this.#yj(Q,j)}#bj(j,Q){if(this.#z(Q,j,1),this.#z(this.element,j,1),this.#q.set(j,(this.#q.get(j)??0)+1),this.#x++===0)this.element.setAttribute("aria-busy","true");for(let X of this.#r(j))this.#z(X,j,1)}#vj(j,Q){this.#z(Q,j,-1),this.#z(this.element,j,-1);let X=(this.#q.get(j)??1)-1;if(X<=0)this.#q.delete(j);else this.#q.set(j,X);if(--this.#x<=0)this.#x=0,this.element.removeAttribute("aria-busy");for(let Z of this.#r(j))this.#z(Z,j,-1)}#z(j,Q,X){if(!j||typeof j.getAttribute!=="function")return;let Z=this.#B.get(j)??new Map,$=(Z.get(Q)??0)+X;if($<=0)Z.delete(Q);else Z.set(Q,$);if(Z.size===0){this.#B.delete(j),j.removeAttribute("data-reactive-busy");return}this.#B.set(j,Z),j.setAttribute("data-reactive-busy",[...Z.keys()].join(" "))}#r(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((X)=>X.getAttribute("data-reactive-busy-on")===j&&this.#Q(X))}#yj(j,Q){let X=this.#_.get(j);if(!X)return;if(--X.count>0)return;if(this.#_.delete(j),!j.isConnected)return;if(Q.disable)j.disabled=X.disabled;if(X.hadText&&j.innerHTML===X.swappedTo)j.innerHTML=X.html}#hj(j){if(!j||typeof j!=="object")return null;let{class:Q,...X}=j;return Q==null?j:{...X,add_class:Q}}#fj(){return this.#t??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#uj(){if(this.#M!=null)return this.#M;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return this.#M=Number.isFinite(Q)&&Q>0?Q:30000}#pj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}#mj(){return document.querySelector("pgbus-stream-source[connection-id]")?.getAttribute("connection-id")||document.querySelector('meta[name="pgbus-connection-id"]')?.content||null}}export{Sj as resetReactiveDefers,r as registerReactiveVisit,t as registerReactiveToken,qj as registerReactiveOffline,e as registerReactiveJs,zj as registerReactiveDismiss,jj as registerReactiveDefer,b as registerReactiveActions,kj as pendingDeferVia,Wj as escapeRegExp,Yj as enableLatencySim,Uj as disableLatencySim,fj as default,Aj as checkReactiveRegistration,yj as __resetReactiveRegistrationForTest,bj as __resetReactiveOfflineForTest,vj as __resetReactiveLatencyForTest,wj as __resetReactiveDismissForTest,hj as __markReactiveConnectedForTest,I as LATENCY_KEY};
|
|
1
|
+
import{Controller as Qj}from"@hotwired/stimulus";import{confirmResolver as b}from"phlex/reactive/confirm";import{computeReducer as Xj}from"phlex/reactive/compute";import{confirmPredicate as Zj}from"phlex/reactive/confirm_predicate";function $j(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:visit"])return;j["reactive:visit"]=function(){let Q=this.getAttribute("data-url");if(Q)window.Turbo.visit(Q,{action:"advance"})}}function Jj(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:token"])return;j["reactive:token"]=function(){let Q=this.getAttribute("data-reactive-token-value"),X=this.getAttribute("target");if(!Q||!X)return;let Z=document.getElementById(X);if(Z)Z.setAttribute("data-reactive-token-value",Q)}}function Gj(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:js"])return;j["reactive:js"]=function(){let Q=e(this.getAttribute("data-reactive-ops"));if(!Q.length)return;let X=this.getAttribute("target"),Z=X?document.getElementById(X):null;if(X&&!Z)return;jj(Q,($)=>wj($,Z))}}var V=new Map;function p(j,Q){let X=!V.has(j);if(V.set(j,Q),X)l()}function O(j){if(V.delete(j))o()}function fj(){V.clear(),D=!1}function pj(j){return V.get(j)?.via}var D=!1;function zj(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:defer"])return;if(j["reactive:defer"]=function(){let Q=this.getAttribute("target");if(!Q)return;if(this.getAttribute("data-reactive-defer-via")==="stream"){qj(Q,this);return}let X=this.getAttribute("data-reactive-defer-token");if(!X)return;P(Q,X)},!D&&typeof document<"u"&&document.addEventListener)D=!0,document.addEventListener("turbo:before-stream-render",Kj)}function Kj(j){let X=j.target?.getAttribute?.("target");if(!X)return;let Z=X.startsWith("reactive-defer-src-")?X.slice(19):X;if(V.get(Z)?.via==="stream")O(Z)}function P(j,Q){let X=document.getElementById(j);if(!X){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}m(j),g(X);let Z={via:"fetch",abort:new AbortController,timedOut:!1};p(j,Z),Yj(j,Z,Q)}function qj(j,Q){let X=document.getElementById(j);if(!X){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}let Z=Q.getAttribute("data-reactive-defer-src");if(!Z)return;if(!globalThis.customElements?.get?.("pgbus-stream-source")){let J=Q.getAttribute("data-reactive-defer-token");if(J){P(j,J);return}console.error("[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered "+"and no fallback token was provided — is the pgbus client loaded on this page?");return}m(j),g(X);let $=document.createElement("pgbus-stream-source");$.id=u(j),$.setAttribute("src",Z),$.setAttribute("since-id",Q.getAttribute("data-reactive-defer-since-id")??"0"),$.setAttribute("hidden",""),document.body.appendChild($),p(j,{via:"stream"})}function u(j){return`reactive-defer-src-${j}`}async function Yj(j,Q,X){let Z=setTimeout(()=>{Q.timedOut=!0,Q.abort.abort()},Wj()),$;try{$=await fetch(Uj(),{method:"POST",headers:{Accept:"text/vnd.turbo-stream.html","Content-Type":"application/json","X-CSRF-Token":Hj()},body:JSON.stringify({token:X}),credentials:"same-origin",signal:Q.abort.signal})}catch(G){if(clearTimeout(Z),V.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed",G),F(j,X);return}if(V.get(j)!==Q){clearTimeout(Z);return}if($.status===204){clearTimeout(Z),v(j);return}if(!$.ok){clearTimeout(Z),console.error(`[phlex-reactive] deferred render failed: HTTP ${$.status}`),F(j,X,$.status);return}let J;try{J=await $.text()}catch(G){if(clearTimeout(Z),V.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed reading the body",G),F(j,X);return}if(clearTimeout(Z),V.get(j)!==Q)return;v(j),window.Turbo.renderStreamMessage(J)}function m(j){let Q=V.get(j);if(!Q)return;if(O(j),Q.via==="fetch")Q.abort.abort();else document.getElementById(u(j))?.remove?.()}function g(j){j.setAttribute("data-reactive-defer-pending","true"),j.setAttribute("aria-busy","true")}function c(j){j.removeAttribute("data-reactive-defer-pending"),j.removeAttribute("aria-busy")}function v(j){O(j);let Q=document.getElementById(j);if(!Q)return;c(Q),Q.removeAttribute("data-reactive-error")}function F(j,Q,X){O(j);let Z=document.getElementById(j);if(!Z)return;c(Z),Z.setAttribute("data-reactive-error","defer");let $=()=>{let J=document.getElementById(j);if(!J){console.warn("[phlex-reactive] defer retry() ignored — the target left the DOM");return}J.removeAttribute("data-reactive-error"),P(j,Q)};Z.dispatchEvent(new CustomEvent("reactive:error",{bubbles:!0,composed:!0,detail:{kind:"defer",target:j,status:X,retry:$}}))}function Uj(){return document.querySelector('meta[name="phlex-reactive-defer-path"]')?.content||"/reactive/defer"}function Hj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}function Wj(){let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:30000}var I=!1;function _j(){if(I)return;if(typeof document>"u"||!document.addEventListener)return;I=!0,document.addEventListener("turbo:before-stream-render",Lj)}function Lj(j){let Q=j.detail,X=Q?.render;if(typeof X!=="function"||X.__reactiveDismissWrapped){if(typeof requestAnimationFrame==="function")requestAnimationFrame(C);else setTimeout(C,0);return}let Z=async($)=>{await X($),C()};Z.__reactiveDismissWrapped=!0,Q.render=Z}function C(){let j=document.querySelectorAll("[data-reactive-dismiss-after]");for(let Q of j){if(Q.hasAttribute("data-reactive-dismiss-scheduled"))continue;let X=Number(Q.getAttribute("data-reactive-dismiss-after"));if(!Number.isFinite(X)||X<=0)continue;Q.setAttribute("data-reactive-dismiss-scheduled",""),setTimeout(()=>Q.remove(),X)}}function uj(){I=!1}var S=!1;function Nj(){if(S)return;if(typeof window>"u"||typeof document>"u")return;if(typeof window.addEventListener!=="function")return;S=!0;let j=()=>{let Q=document.documentElement;if(typeof Q?.toggleAttribute!=="function")return;Q.toggleAttribute("data-reactive-offline",globalThis.navigator?.onLine===!1)};j(),window.addEventListener("online",j),window.addEventListener("offline",j)}function mj(){S=!1}var k="phlex-reactive:latency",x=!1;function Vj(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(k,String(j))}function Aj(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(k),x=!1}function Mj(){if(typeof window>"u"||typeof document>"u")return;if(document.querySelector?.('meta[name="phlex-reactive-env"]')?.content!=="development")return;window.PhlexReactive={enableLatencySim:Vj,disableLatencySim:Aj}}function gj(){x=!1}var d="data-reactive-active",A=0;function l(){if(A++,A===1)s("reactive:busy")}function o(){if(A===0)return;if(A--,A===0)s("reactive:idle")}function cj(){return A}function dj(){A=0,(typeof document<"u"?document.documentElement:null)?.removeAttribute?.(d)}function s(j){if(typeof document>"u")return;let Q=document.documentElement;if(typeof Q?.toggleAttribute==="function")Q.toggleAttribute(d,A>0);if(typeof document.dispatchEvent==="function"&&typeof CustomEvent==="function")document.dispatchEvent(new CustomEvent(j,{detail:{count:A}}))}function h(){$j(),Jj(),Gj(),zj(),_j(),Nj(),Mj()}function Bj(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)h();else document.addEventListener("turbo:load",h,{once:!0});var E=!1;function xj(){if(E)return;if(typeof document>"u")return;let j=document.querySelectorAll('[data-controller~="reactive"]');if(!j||j.length===0)return;console.warn("[phlex-reactive] found "+j.length+' element(s) with data-controller="reactive" '+"but the reactive controller never connected. It is loaded but not registered — "+'add `application.register("reactive", ReactiveController)` (importmap) or import it into app/javascript/controllers/ for lazyLoadControllersFrom apps. See the README.')}function lj(){E=!1}function oj(){E=!0}if(typeof window<"u"&&typeof document<"u"){let j=()=>setTimeout(xj,0);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",j,{once:!0});else j()}var Oj=new Set(["href","src","srcdoc","action","formaction","xlink:href","style"]);function Pj(j){let Q=String(j).toLowerCase();return Q.startsWith("on")||Oj.has(Q)}function Ej(j,Q,X){let[Z,$,J]=Q;j.classList.add(Z,$),X(),requestAnimationFrame(()=>{j.classList.remove($),j.classList.add(J)});let G=!1,K=()=>{if(G)return;G=!0,j.classList.remove(Z,J)};j.addEventListener("animationend",K,{once:!0}),setTimeout(K,350)}var y=Object.freeze({show:(j,Q)=>R(j,!1,Q),hide:(j,Q)=>R(j,!0,Q),toggle:(j,Q)=>R(j,!j.hidden,Q),add_class:(j,Q)=>j.classList.add(...Q.classes??[]),remove_class:(j,Q)=>j.classList.remove(...Q.classes??[]),toggle_class:(j,Q)=>(Q.classes??[]).forEach((X)=>j.classList.toggle(X)),set_attr:(j,Q)=>{if(T(Q.name))j.setAttribute(Q.name,Q.value??"")},remove_attr:(j,Q)=>{if(T(Q.name))j.removeAttribute(Q.name)},toggle_attr:(j,Q)=>{if(!T(Q.name))return;if(j.hasAttribute(Q.name))j.removeAttribute(Q.name);else j.setAttribute(Q.name,"")},focus:(j)=>j.focus?.(),focus_first:(j)=>kj(j)?.focus?.(),text:(j,Q)=>{let X=String(Q.value??"");if(j.textContent!==X)j.textContent=X},dispatch:(j,Q)=>{j.dispatchEvent(new CustomEvent(Q.name,{bubbles:!0,composed:!0,detail:Q.detail??{}}))}});function R(j,Q,X){if(X?.transition)Ej(j,X.transition,()=>j.hidden=Q);else j.hidden=Q}function T(j){if(!Pj(j))return!0;return console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(j)} — skipped`),!1}var i=/^#[A-Za-z_][\w-]*$/;function Fj(j){if(typeof j==="string"&&i.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(j)} — skipped`),!1}function Cj(j,Q){let X=j.getAttribute("data-reactive-show-equals");if(X!==null)return Q===X;let Z=j.getAttribute("data-reactive-show-not");if(Z!==null)return Q!==Z;let $=j.getAttribute("data-reactive-show-in");if($!==null){try{let J=JSON.parse($);if(Array.isArray(J))return J.includes(Q)}catch{}return console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify($)} — skipped`),null}for(let J of n){let G=j.getAttribute(`data-reactive-show-${J}`);if(G!==null)return a(J,G,Q)}return console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped"),null}var n=["gte","gt","lte","lt"];function a(j,Q,X){let Z=Number(Q);if(Number.isNaN(Z))return console.warn(`[phlex-reactive] reactive_show ${j}: needs a numeric literal, got ${JSON.stringify(Q)} — skipped`),null;let $=X==null?"":String(X).trim(),J=$===""?NaN:Number($);if(Number.isNaN(J))return!1;switch(j){case"gte":return J>=Z;case"gt":return J>Z;case"lte":return J<=Z;case"lt":return J<Z;default:return null}}function r(j,Q){if(!j||typeof j!=="object")return null;if(typeof j.equals==="string")return Q===j.equals;if(typeof j.not==="string")return Q!==j.not;if(Array.isArray(j.in))return j.in.includes(Q);for(let X of n)if(X in j)return a(X,j[X],Q);return null}var f="[data-reactive-show-field], [data-reactive-show]";function Rj(j){try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn(`[phlex-reactive] malformed compound reactive_show payload ${JSON.stringify(j)} — skipped`),null}function w(j,Q){if(!j||typeof j!=="object"||typeof j.field!=="string")return!1;let X=Q(j.field)??"";return r(j,X)===!0}function t(j,Q){if(!Array.isArray(j)||j.length===0)return null;return j.some((X)=>Array.isArray(X)&&X.length>0&&X.every((Z)=>w(Z,Q)))}function Tj(j,Q){if(!j||typeof j!=="object")return null;let X=j.any;if(Array.isArray(X)&&(X.length===0||Array.isArray(X[0])))return t(X,Q);return Dj(j,Q)}function Dj(j,Q){let X=Array.isArray(j.all)?"all":Array.isArray(j.any)?"any":null;if(!X)return null;let Z=j[X];if(Z.length===0)return null;let $=Z.map((J)=>w(J,Q));return X==="all"?$.every(Boolean):$.some(Boolean)}function Ij(j){if(typeof j==="string"&&i.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root show target ${JSON.stringify(j)} — skipped`),!1}var Sj='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function kj(j){return j.querySelectorAll?.(Sj)?.[0]??null}function e(j){if(Array.isArray(j))return j;if(typeof j!=="string")return[];try{let Q=JSON.parse(j);return Array.isArray(Q)?Q:[]}catch{return[]}}function jj(j,Q){for(let X of j){if(!Array.isArray(X))continue;let[Z,$={}]=X;if(!Object.hasOwn(y,Z)){console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(Z)} — skipped`);continue}for(let J of Q($))y[Z](J,$)}}function wj(j,Q){let X=j.to;if(Q){if(X==="@root")return[Q];if(typeof X!=="string"||X==="")return[];if(j.global)return[...document.querySelectorAll(X)];return[...Q.querySelectorAll(X)]}if(typeof X!=="string"||X===""||X==="@root")return[];return[...document.querySelectorAll(X)]}class sj extends Qj{static values={token:String};#R;#L=new Map;#K=new Map;#e;#B;#T;#x=0;#q=new Map;#O=new WeakMap;#N=new Map;#D=new WeakSet;#Y;#U;#H;#j;#X;#W;#V;connect(){if(E=!0,this.element.id==="")console.warn("[phlex-reactive] a reactive root has no id; its next-action token can't self-match "+"and may fall back to the first token in the response → a silent HTTP 403 on the NEXT action. "+"Put id: on the SAME element as reactive_attrs — use div(**reactive_root) (emits id + token together), "+"or div(id:, **reactive_attrs). The id: must NOT be on a child. See the README.");if(this.element.getAttribute?.("data-reactive-defer-token"))this.#I(),this.#V=()=>this.#I(),this.element.addEventListener?.("turbo:morph-element",this.#V);if(this.#jj()){if(this.#Y=()=>this.#F(),this.element.addEventListener?.("turbo:morph-element",this.#Y),this.#F(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#Bj()}if(this.#Oj())this.#j=()=>this.#g(),this.element.addEventListener?.("input",this.#j),this.element.addEventListener?.("change",this.#j),this.element.addEventListener?.("turbo:morph-element",this.#j),this.#g();if(this.#Cj())this.#X=(j)=>{if(j?.type==="input"&&!this.#Tj(j))return;this.#l()},this.element.addEventListener?.("input",this.#X),this.element.addEventListener?.("turbo:morph-element",this.#X),this.#l();if(this.#Rj())this.#W=()=>this.recompute(),this.element.addEventListener?.("turbo:morph-element",this.#W),this.recompute()}#jj(){if((this.element.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let j=this.element.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}disconnect(){if(this.#qj(),this.#Uj(),this.#xj(),this.#Fj(),this.#Dj(),this.#Ij(),this.#V)this.element.removeEventListener?.("turbo:morph-element",this.#V)}#I(){let j=this.element;if(!j?.id)return;let Q=j.getAttribute?.("data-reactive-defer-token");if(!Q)return;if(j.getAttribute?.("data-reactive-defer-pending")!=="true")return;P(j.id,Q)}dispatch(j){let{action:Q,params:X,debounce:Z,throttle:$,confirm:J,confirmWhen:G,outside:K,window:z,optimistic:Y}=j.params;if(!Q)return;let H=j.params.busy??this.#uj(j.params.loading);if(K&&this.element.contains(j.target))return;let L=j.currentTarget??j.target;if(!z&&!this.#wj(Y,L))j.preventDefault();let W=this.#p(J,G);if(!W)return this.#b(L,Q,X,Z,$,Y,H);Promise.resolve().then(()=>b(W)).catch(()=>!1).then((_)=>{if(_)this.#b(L,Q,X,Z,$,Y,H)})}runOps(j){let{ops:Q,confirm:X,confirmWhen:Z,outside:$,window:J}=j.params;if($&&this.element.contains(j.target))return;if(!J)j.preventDefault();let G=this.#p(X,Z);if(!G)return this.#i(this.#s(Q));Promise.resolve().then(()=>b(G)).catch(()=>!1).then((K)=>{if(K)this.#i(this.#s(Q))})}trackDirty(){this.#F()}recompute(j){if(j&&this.#D.has(j))return;let Q=this.#Xj(),X=Q.map(([q])=>q),Z=this.element.getAttribute?.("data-reactive-scope")||null,$=(q)=>Z&&!q.includes("[")?`${Z}[${q}]`:q,J=this.#_(),G=new Map,K=(q)=>{if(G.has(q))return G.get(q);let U=null;for(let M of this.element.querySelectorAll(`[name="${$(q)}"]`))if(J(M)){U=M;break}return G.set(q,U),U};for(let q of X)this.#k(q,K(q)?.value??"");let z=this.element.getAttribute("data-reactive-compute-reducer-param"),Y=z?Xj(z):null;if(!Y){this.#w({},K);return}let H=this.#Qj("data-reactive-compute-outputs-param"),L={};for(let[q,U]of Q){let M=K(q);if(U==="string")L[q]=M?.value??"";else{let N=Number(M?.value);L[q]=Number.isFinite(N)?N:0}}let W=Y(L,{changed:this.#Zj(j,X,Z)})||{},_=[];for(let q of H){if(!(q in W))continue;let U=K(q);if(!U)continue;if(String(W[q])===U.value)continue;U.value=W[q],_.push(U)}for(let q of Object.keys(W)){let U=W[q];if(U===void 0||U===null)continue;this.#k(q,U)}this.#w(W,K);for(let q of _){let U=new Event("input",{bubbles:!0});this.#D.add(U),q.dispatchEvent(U)}}listnavNext(j){this.#S(j,1)}listnavPrev(j){this.#S(j,-1)}listnavPick(j){let Q=this.#P(j),X=Q.findIndex((Z)=>Z.hasAttribute("data-reactive-highlighted"));if(X<0)return;j.preventDefault(),Q[X].click()}listnavClose(j){for(let Q of this.#P(j))Q.removeAttribute("data-reactive-highlighted")}#S(j,Q){let X=this.#P(j);if(!X.length)return;j.preventDefault();let Z=X.findIndex((G)=>G.hasAttribute("data-reactive-highlighted")),$=Z<0?Q>0?0:X.length-1:(Z+Q+X.length)%X.length;for(let G of X)G.removeAttribute("data-reactive-highlighted");let J=X[$];J.setAttribute("data-reactive-highlighted","true"),J.scrollIntoView?.({block:"nearest"})}#P(j){let X=(j?.currentTarget??j?.target??this.element).getAttribute?.("data-reactive-listnav-option-param")??this.element.getAttribute("data-reactive-listnav-option-param");if(!X)return[];let Z=this.#_();return Array.from(this.element.querySelectorAll(X)).filter(($)=>!$.hidden&&Z($))}#Qj(j){let Q=this.element.getAttribute(j);if(!Q)return[];try{let X=JSON.parse(Q);return Array.isArray(X)?X:[]}catch{return[]}}#Xj(){let j=this.element.getAttribute("data-reactive-compute-inputs-param");if(!j)return[];try{let Q=JSON.parse(j);if(Array.isArray(Q))return Q.map((X)=>[X,"number"]);if(Q&&typeof Q==="object")return Object.entries(Q);return[]}catch{return[]}}#Zj(j,Q,X){let Z=j?.target;if(!Z?.name||typeof Z.closest!=="function")return null;let $=this.#$j(Z.name,X);if(!Q.includes($))return null;return this.#Q(Z)?$:null}#$j(j,Q){if(!Q)return j;let X=`${Q}[`;return j.startsWith(X)&&j.endsWith("]")?j.slice(X.length,-1):j}#k(j,Q){let X=String(Q);for(let Z of this.#Jj(j)){if(Z.textContent===X)continue;Z.textContent=X}}#Jj(j){let Q=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(Q).filter((X)=>this.#Q(X))}#w(j,Q){let X=this.#Gj();for(let[Z,$]of Object.entries(X)){let J=Z in j?j[Z]:Q(Z)?.value;if(J===void 0||J===null)continue;let G=String(J);for(let K of Array.isArray($)?$:[$]){if(!Fj(K))continue;for(let z of document.querySelectorAll(K)){if(z.textContent===G)continue;z.textContent=G}}}}#Gj(){let j=this.element.getAttribute("data-reactive-compute-mirror-param");if(!j)return{};try{let Q=JSON.parse(j);return Q&&typeof Q==="object"&&!Array.isArray(Q)?Q:{}}catch{return{}}}#b(j,Q,X,Z,$,J,G){if(this.#M("reactive:before-dispatch",{action:Q,params:this.#o(X),element:this.element},{cancelable:!0}).defaultPrevented)return;let z=Number(Z)||0;if(z>0)return this.#Kj(j,z,Q,X,J,G);let Y=Number($)||0;if(Y>0)return this.#Yj(j,Y,Q,X,J,G);return this.#A(Q,X,J,j,G)}#A(j,Q,X,Z,$){let J=this.#bj(X,Z),G=this.#vj(j,Z,$),K=this.#v()?this.#zj(X,Z):null;return this.queue=(this.queue??Promise.resolve()).then(()=>this.#Vj(j,Q,J,G,K)),this.queue}#zj(j,Q){if(!j?.hide)return null;let X=this.#r(j,Q);if(!X.length)return null;return()=>{let Z=X.filter(($)=>$.isConnected&&!$.hidden);if(!Z.length)return;console.warn("[phlex-reactive] optimistic: { hide: true } was undone by the reply's re-render — "+"the element is visible again. For an instant delete, return reply.remove so the server removes it; otherwise the hide only flashes.",Z)}}#Kj(j,Q,X,Z,$,J){this.#E(j);let G=()=>{this.#E(j),this.#A(X,Z,$,j,J)},K=setTimeout(G,Q);j?.addEventListener?.("blur",G,{once:!0}),this.#L.set(j,{timer:K,flush:G})}#E(j){let Q=this.#L.get(j);if(!Q)return;clearTimeout(Q.timer),j?.removeEventListener?.("blur",Q.flush),this.#L.delete(j)}#qj(){for(let j of[...this.#L.keys()])this.#E(j)}#Yj(j,Q,X,Z,$,J){let G=this.#K.get(j)??new Map;if(G.has(X))return;let K=setTimeout(()=>{if(G.delete(X),G.size===0)this.#K.delete(j)},Q);return G.set(X,K),this.#K.set(j,G),this.#A(X,Z,$,j,J)}#Uj(){for(let j of this.#K.values())for(let Q of j.values())clearTimeout(Q);this.#K.clear()}#M(j,Q,{cancelable:X=!1}={}){let Z=new CustomEvent(j,{bubbles:!0,composed:!0,cancelable:X,detail:Q});return(this.element.isConnected?this.element:document).dispatchEvent(Z),Z}#Z(j,Q,X,Z){let $=()=>{if(!this.element.isConnected){console.warn("[phlex-reactive] retry() ignored — the reactive root left the DOM");return}return this.#A(j,Q)};this.#M("reactive:error",{action:j,params:X,...Z,retry:$})}#$(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#Hj(){this.element?.removeAttribute?.("data-reactive-error")}#Wj(){let j=document.querySelector("[data-reactive-error-flash]");if(!j?.content)return;let Q=j.getAttribute("data-reactive-error-flash")||"flash",X=document.getElementById(Q);if(!X)return;X.appendChild(j.content.cloneNode(!0))}#_j(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(k));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!x)x=!0,console.warn(`[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${j}ms. Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.`);return new Promise((Q)=>setTimeout(Q,j))}#v(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#h(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#Lj(j){if(!j)return[];let Q=[],X=/<turbo-stream\b([^>]*)>/g,Z;while((Z=X.exec(j))!==null){let $=Z[1],J=$.match(/\baction="([^"]*)"/)?.[1]??"?",G=$.match(/\btarget="([^"]*)"/)?.[1];Q.push(G?`${J} → #${G}`:J)}return Q}#Nj(j){let{action:Q,status:X,ms:Z}=j,J=`reactive ${this.element?.id?`#${this.element.id} `:""}${Q} → ${X??"—"} (${Math.round(Z)}ms)`;if(console.groupCollapsed(J),console.log(`params: [${j.paramNames.join(", ")}] + collected: [${j.fieldNames.join(", ")}]`),console.log(`encoding: ${j.encoding}`),j.streams.length)console.log(`streams: ${j.streams.join(" ")}`);console.log(`token: ${j.tokenRefreshed?"refreshed ✓":"unchanged"}`),console.groupEnd()}async#Vj(j,Q,X,Z,$){let{fields:J,files:G}=this.#u(),K=this.#o(Q),z={...J,...K},Y=this.#J,H=G.length>0,L=H?this.#Sj(Y,j,z,G):JSON.stringify({token:Y,act:j,params:z}),W=this.#v()?{action:j,paramNames:Object.keys(K),fieldNames:Object.keys(J),encoding:H?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#h()}:null;await this.#_j();try{if(navigator.onLine===!1){this.#G(X),this.#$("offline"),this.#Z(j,Q,z,{kind:"offline"});return}let _;try{let N={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#cj()};if(!H)N["Content-Type"]="application/json";let B=this.#dj();if(B)N["X-Pgbus-Connection"]=B;_=await fetch(this.#mj(),{method:"POST",headers:N,body:L,credentials:"same-origin",signal:AbortSignal.timeout(this.#gj())})}catch(N){if(console.error("[phlex-reactive] action error",N),this.#G(X),N?.name==="TimeoutError"||N?.name==="AbortError"){this.#$("timeout"),this.#Z(j,Q,z,{kind:"timeout"});return}this.#Wj(),this.#$("network"),this.#Z(j,Q,z,{kind:"network"});return}if(W)W.status=_.status;if(_.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#G(X),this.#$("redirected"),this.#Z(j,Q,z,{kind:"redirected",status:_.status});return}if(!_.ok){let N=await _.text();if(console.error(`[phlex-reactive] action failed: HTTP ${_.status}`,N),this.#G(X),(_.headers.get("Content-Type")||"").includes("turbo-stream")){let B=this.#f(N);if(this.#J=B??this.#J,W)this.#y(W,N,B);window.Turbo.renderStreamMessage(N)}this.#$("http"),this.#Z(j,Q,z,{kind:"http",status:_.status,body:N});return}let q=_.headers.get("Content-Type")||"";if(!q.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${q}" — no update applied`),this.#G(X),this.#$("content-type"),this.#Z(j,Q,z,{kind:"content-type",status:_.status});return}let U=await _.text(),M=this.#f(U);if(this.#J=M??this.#J,W)this.#y(W,U,M);if(window.Turbo.renderStreamMessage(U),$)queueMicrotask($);this.#Hj(),this.#M("reactive:applied",{action:j,params:z,html:U})}catch(_){console.error("[phlex-reactive] action error",_),this.#G(X),this.#M("reactive:error",{action:j,params:z,kind:"apply"})}finally{if(Z?.(),W)this.#Nj({...W,ms:this.#h()-W.started})}}#y(j,Q,X){j.streams=this.#Lj(Q),j.tokenRefreshed=X!=null}get#J(){return this.#R??this.tokenValue}set#J(j){this.#R=j}#f(j){let Q=this.element.id;if(!Q)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:X,self:Z}=this.#Aj(Q),$=j.match(X);if($)return $[1];let J=j.match(Z);if(J)return J[1].match(/data-reactive-token-value="([^"]+)"/)?.[1];return}#Aj(j){let Q=this.#T;if(Q&&Q.id===j)return Q;let X=Bj(j);return this.#T={id:j,token:new RegExp(`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${X}"[^>]*\\bdata-reactive-token-value="([^"]+)"`),self:new RegExp(`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${X}"[^>]*>([\\s\\S]*?)</turbo-stream>`)}}#Q(j){return j.closest('[data-controller~="reactive"]')===this.element}#_(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(Q)=>this.#Q(Q)}#p(j,Q){if(j)return j;if(!Q)return null;let X=Q;if(typeof Q==="string")try{X=JSON.parse(Q)}catch{return console.warn(`[phlex-reactive] malformed conditional confirm payload ${JSON.stringify(Q)} — skipped`),null}if(!X||typeof X!=="object")return null;let{fields:Z}=this.#u(),$=(G)=>Z[G],J;if(typeof X.predicate==="string"){let G=Zj(X.predicate);if(!G)return console.warn(`[phlex-reactive] confirm predicate "${X.predicate}" is not registered — proceeding without a dialog (register it with setConfirmPredicate)`),null;J=!!G(Z)}else J=t(X.groups?.any,$)===!0;return J?X.message:null}#u(){let j={},Q=[],X=this.#_();return this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Z)=>{if(!X(Z))return;if(Z.type==="file")for(let $ of Z.files??[])Q.push({name:Z.name,file:$,multiple:Z.multiple});else if(Z.type==="checkbox")j[Z.name]=Z.checked;else if(Z.type==="radio"){if(Z.checked)j[Z.name]=Z.value}else j[Z.name]=Z.value}),this.element.querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])").forEach((Z)=>{if(!X(Z))return;let $=Z.getAttribute("name");if(!$)return;let J=j[$];if(J==null||J==="")j[$]=Z.value??Z.textContent??Z.innerHTML??""}),{fields:j,files:Q}}#F(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Q)=>{if(!this.#Q(Q))return;if(Q.type==="file")return;if(this.#Mj(Q))Q.setAttribute("data-reactive-dirty","true"),j++;else Q.removeAttribute("data-reactive-dirty")}),j>0)this.element.setAttribute("data-reactive-dirty",String(j));else this.element.removeAttribute("data-reactive-dirty")}#Mj(j){if(j.type==="checkbox"||j.type==="radio")return j.checked!==j.defaultChecked;if(j.tag==="select"||j.options)return Array.from(j.options??[]).some((Q)=>Q.selected!==Q.defaultSelected);return j.value!==j.defaultValue}#m(){let j=this.element.getAttribute?.("data-reactive-dirty"),Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:0}#Bj(){if(typeof window>"u"||typeof window.addEventListener!=="function")return;this.#U=(j)=>{if(this.#m()===0)return;return j.preventDefault(),j.returnValue="You have unsaved changes.",j.returnValue},this.#H=(j)=>{if(this.#m()===0)return;if(!(typeof window.confirm==="function"?window.confirm("You have unsaved changes. Leave anyway?"):!0))j.preventDefault?.()},window.addEventListener("beforeunload",this.#U),window.addEventListener("turbo:before-visit",this.#H)}#xj(){if(this.#Y)this.element.removeEventListener?.("turbo:morph-element",this.#Y),this.#Y=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#U)window.removeEventListener("beforeunload",this.#U);if(this.#H)window.removeEventListener("turbo:before-visit",this.#H)}this.#U=void 0,this.#H=void 0}#Oj(){if(this.element.getAttribute?.("data-reactive-show-targets"))return!0;let j=this.element.querySelectorAll?.(f)??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}#g(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.#_(),Q=this.element.getAttribute?.("data-reactive-scope")||null,X=new Map,Z=($)=>{if(!X.has($))X.set($,this.#d($,j,Q));return X.get($)};for(let $ of this.element.querySelectorAll(f)){if(!j($))continue;let J=$.getAttribute("data-reactive-show");if(J!==null){let Y=Tj(Rj(J),Z);if(Y!==null)this.#c($,Y,j,Q);continue}let G=$.getAttribute("data-reactive-show-field");if(!G)continue;let K=Z(G);if(K===null)continue;let z=Cj($,K);if(z===null)continue;this.#c($,z,j,Q)}this.#Pj(j,X,Q)}#c(j,Q,X,Z){if(j.hidden=!Q,j.getAttribute("data-reactive-show-disable")!=="true")return;if(typeof j.querySelectorAll!=="function")return;for(let $ of j.querySelectorAll("input[name], select[name], textarea[name]"))if(X($))$.disabled=!Q;if(j.name&&X(j))j.disabled=!Q}#Pj(j,Q,X){let Z=this.#Ej();for(let[$,J]of Object.entries(Z)){if(!J||typeof J!=="object"||Array.isArray(J))continue;if(!Q.has($))Q.set($,this.#d($,j,X));let G=Q.get($);if(G===null)continue;let K=()=>G;for(let[z,Y]of Object.entries(J)){if(!Ij(z))continue;let H;if(Array.isArray(Y)){if(Y.length===0){console.warn(`[phlex-reactive] malformed reactive_show_targets group for ${z} — skipped`);continue}H=Y.every((L)=>w(L,K))}else{let L=r(Y,G);if(L===null){console.warn(`[phlex-reactive] malformed reactive_show_targets predicate for ${z} — skipped`);continue}H=L}for(let L of document.querySelectorAll(z))L.hidden=!H}}}#Ej(){let j=this.element.getAttribute?.("data-reactive-show-targets");if(!j)return{};try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn("[phlex-reactive] malformed data-reactive-show-targets — ignored. "+"Did two reactive_show_targets calls collide on one root? Declare every field in ONE call: reactive_show_targets(mode: { ... }, kind: { ... })"),{}}#d(j,Q,X){let Z=X&&!j.includes("[")?`${X}[${j}]`:j,$=!1,J=null;for(let G of this.element.querySelectorAll(`[name="${Z}"]`)){if(!Q(G))continue;if(G.type==="checkbox")return G.checked?"true":"false";if(G.type==="radio"){if(G.checked)return G.value??"";$=!0;continue}J??=G}if(J)return J.value??"";return $?"":null}#Fj(){if(!this.#j)return;this.element.removeEventListener?.("input",this.#j),this.element.removeEventListener?.("change",this.#j),this.element.removeEventListener?.("turbo:morph-element",this.#j),this.#j=void 0}#Cj(){return!!(this.element.getAttribute?.("data-reactive-filter-input")&&this.element.getAttribute?.("data-reactive-filter-option"))}#Rj(){return this.element.getAttribute?.("data-reactive-compute-seed")==="true"}#Tj(j){let Q=this.element.getAttribute("data-reactive-filter-input");return!!Q&&typeof j.target?.matches==="function"&&j.target.matches(Q)}#l(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.element.getAttribute("data-reactive-filter-input"),Q=this.element.getAttribute("data-reactive-filter-option");if(!j||!Q)return;let X=this.#_(),Z=[...this.element.querySelectorAll(j)].find(X);if(!Z)return;let $=(Z.value??"").trim().toLowerCase(),J=0;for(let z of this.element.querySelectorAll(Q)){if(!X(z))continue;let Y=(z.getAttribute("data-reactive-filter-text")??z.textContent??"").toLowerCase(),H=$!==""&&!Y.includes($);if(z.hidden=H,H)z.removeAttribute("data-reactive-highlighted");else J++}let G=this.element.getAttribute("data-reactive-filter-group");if(G)for(let z of this.element.querySelectorAll(G)){if(!X(z))continue;let Y=[...z.querySelectorAll(Q)].filter(X);if(Y.length===0)continue;z.hidden=Y.every((H)=>H.hidden)}let K=this.element.getAttribute("data-reactive-filter-empty");if(K){for(let z of this.element.querySelectorAll(K))if(X(z))z.hidden=J>0}}#Dj(){if(!this.#X)return;this.element.removeEventListener?.("input",this.#X),this.element.removeEventListener?.("turbo:morph-element",this.#X),this.#X=void 0}#Ij(){if(!this.#W)return;this.element.removeEventListener?.("turbo:morph-element",this.#W),this.#W=void 0}#Sj(j,Q,X,Z){let $=new FormData;$.append("token",j),$.append("act",Q);for(let[G,K]of Object.entries(X))this.#C($,`params[${G}]`,K);let J=this.#kj(Z);for(let{name:G,file:K,multiple:z}of Z){let H=z||J.has(G)?`params[${G}][]`:`params[${G}]`;$.append(H,K,K.name)}return $}#C(j,Q,X){if(X==null)j.append(Q,"");else if(Array.isArray(X))X.forEach((Z,$)=>this.#C(j,`${Q}[${$}]`,Z));else if(typeof X==="object")for(let[Z,$]of Object.entries(X))this.#C(j,`${Q}[${Z}]`,$);else j.append(Q,String(X))}#kj(j){let Q=new Map;for(let{name:X}of j)Q.set(X,(Q.get(X)??0)+1);return new Set([...Q].filter(([,X])=>X>1).map(([X])=>X))}#o(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#s(j){return e(j)}#i(j){jj(j,(Q)=>this.#n(Q))}#n(j){let Q=j.to;if(Q==="@root")return[this.element];if(typeof Q!=="string"||Q==="")return[];if(j.global)return[...document.querySelectorAll(Q)];return[...this.element.querySelectorAll(Q)].filter((X)=>this.#Q(X))}#wj(j,Q){if(j?.checked!=="keep")return!1;let X=Q?.type;return X==="checkbox"||X==="radio"}#bj(j,Q){if(!j)return null;let X=this.#a(j,Q,!0);return X.length?X:null}#G(j){if(!j)return;if(!this.element.isConnected)return;for(let Q of j)Q()}#vj(j,Q,X){this.#yj(j,Q);let Z=X?this.#a(X,Q,!1):[];l();let $=!1;return()=>{if($)return;$=!0,this.#fj(j,Q),o();for(let J of Z)J()}}#a(j,Q,X){let Z=[];for(let $ of this.#r(j,Q)){if(j.add_class){let J=j.add_class.filter((G)=>!$.classList.contains(G));if($.classList.add(...J),J.length)Z.push(()=>$.classList.remove(...J))}if(j.remove_class){let J=j.remove_class.filter((G)=>$.classList.contains(G));if($.classList.remove(...J),J.length)Z.push(()=>$.classList.add(...J))}if(j.toggle_class)j.toggle_class.forEach((J)=>$.classList.toggle(J)),Z.push(()=>j.toggle_class.forEach((J)=>$.classList.toggle(J)));if(j.hide)$.hidden=!0,Z.push(()=>$.hidden=!1);if(j.show)$.hidden=!1,Z.push(()=>$.hidden=!0)}if(Q&&(j.disable||j.text!=null))Z.push(this.#hj(j,Q));if(X&&j.checked==="keep"&&Q&&"checked"in Q){let $=Q.checked;Z.push(()=>Q.checked=!$)}return Z}#r(j,Q){if(j.to==null)return Q?[Q]:[];return this.#n({to:j.to})}#hj(j,Q){let X=this.#N.get(Q);if(X)X.count++;else this.#N.set(Q,{count:1,disabled:Q.disabled,html:Q.innerHTML,hadText:j.text!=null,swappedTo:j.text});if(j.disable)Q.disabled=!0;if(j.text!=null)Q.innerHTML=j.text;return()=>this.#pj(Q,j)}#yj(j,Q){if(this.#z(Q,j,1),this.#z(this.element,j,1),this.#q.set(j,(this.#q.get(j)??0)+1),this.#x++===0)this.element.setAttribute("aria-busy","true");for(let X of this.#t(j))this.#z(X,j,1)}#fj(j,Q){this.#z(Q,j,-1),this.#z(this.element,j,-1);let X=(this.#q.get(j)??1)-1;if(X<=0)this.#q.delete(j);else this.#q.set(j,X);if(--this.#x<=0)this.#x=0,this.element.removeAttribute("aria-busy");for(let Z of this.#t(j))this.#z(Z,j,-1)}#z(j,Q,X){if(!j||typeof j.getAttribute!=="function")return;let Z=this.#O.get(j)??new Map,$=(Z.get(Q)??0)+X;if($<=0)Z.delete(Q);else Z.set(Q,$);if(Z.size===0){this.#O.delete(j),j.removeAttribute("data-reactive-busy");return}this.#O.set(j,Z),j.setAttribute("data-reactive-busy",[...Z.keys()].join(" "))}#t(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((X)=>X.getAttribute("data-reactive-busy-on")===j&&this.#Q(X))}#pj(j,Q){let X=this.#N.get(j);if(!X)return;if(--X.count>0)return;if(this.#N.delete(j),!j.isConnected)return;if(Q.disable)j.disabled=X.disabled;if(X.hadText&&j.innerHTML===X.swappedTo)j.innerHTML=X.html}#uj(j){if(!j||typeof j!=="object")return null;let{class:Q,...X}=j;return Q==null?j:{...X,add_class:Q}}#mj(){return this.#e??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#gj(){if(this.#B!=null)return this.#B;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return this.#B=Number.isFinite(Q)&&Q>0?Q:30000}#cj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}#dj(){return document.querySelector("pgbus-stream-source[connection-id]")?.getAttribute("connection-id")||document.querySelector('meta[name="pgbus-connection-id"]')?.content||null}}export{fj as resetReactiveDefers,dj as resetReactiveActivity,$j as registerReactiveVisit,Jj as registerReactiveToken,Nj as registerReactiveOffline,Gj as registerReactiveJs,_j as registerReactiveDismiss,zj as registerReactiveDefer,h as registerReactiveActions,cj as reactiveActivityCount,pj as pendingDeferVia,o as exitReactiveActivity,Bj as escapeRegExp,l as enterReactiveActivity,Vj as enableLatencySim,Aj as disableLatencySim,sj as default,xj as checkReactiveRegistration,lj as __resetReactiveRegistrationForTest,mj as __resetReactiveOfflineForTest,gj as __resetReactiveLatencyForTest,uj as __resetReactiveDismissForTest,oj as __markReactiveConnectedForTest,k as LATENCY_KEY,d as ACTIVE_ATTR};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=E7BCDAE133AC4CB864756E2164756E21
|
|
4
4
|
//# sourceMappingURL=reactive_controller.min.js.map
|