phlex-reactive 0.9.1 → 0.9.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 +23 -0
- data/README.md +51 -1
- data/app/javascript/phlex/reactive/reactive_controller.js +80 -4
- 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/dsl.rb +62 -3
- data/lib/phlex/reactive/component/helpers.rb +16 -6
- data/lib/phlex/reactive/component.rb +6 -1
- data/lib/phlex/reactive/js.rb +14 -0
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f48d899c85c31d10e7b864e0b4bd290258c3ccbcb4f97fa6052979bba3b68b1
|
|
4
|
+
data.tar.gz: 39b93e42a39480822ec472e9211e5333ca448cec767d38f6c43569c8edcb09fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22c03f299c2cf95e83cbbf9bd629a9d52308bc2631de44b76eab06fecee6b81313fdd2af6b4cf1de6574c533b0b3c059577ca258b0e2986bf254e5b56bbf1578
|
|
7
|
+
data.tar.gz: 837312de90f9e82b785133de6b2fd69b7dbe2c44d34ce45bb68125469ad1d65daef08791a92c5b438a0e33dbb492dffd29638f638e07f47b8200eb4edd1abee2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Cross-root text mirrors + the `text` client op (#159).** A derived value can
|
|
12
|
+
now be painted into a text node **outside** the computing component's reactive
|
|
13
|
+
root — the read-only recap in another tab pane that previously forced a
|
|
14
|
+
bespoke JS listener — with the library's default-deny posture intact:
|
|
15
|
+
- `reactive_compute ..., mirror: { sum_total: "#sum_total" }` declares
|
|
16
|
+
allowlisted cross-root text mirrors. Each compute pass paints every declared
|
|
17
|
+
name into its document-wide **id** target(s) via `textContent`
|
|
18
|
+
(change-guarded, never `innerHTML`, never blanks a name the pass produced no
|
|
19
|
+
value for). The value comes from the reducer result, a just-written output's
|
|
20
|
+
field, or a declared input's identity value — so it works with no reducer at
|
|
21
|
+
all. Non-id selectors raise at declare time AND are warn-and-skipped by the
|
|
22
|
+
client interpreter (two-sided default-deny). No `mirror:` → the wire is
|
|
23
|
+
byte-identical to before.
|
|
24
|
+
- `js.text(to, value, global: false)` — a new op that sets `textContent`
|
|
25
|
+
(stringified; `nil` clears), available to `on_client`, `reply.js`, and
|
|
26
|
+
`broadcast_js_to`. Strictly less powerful than `set_attr`; pair with
|
|
27
|
+
`global: true` for the cross-root paint.
|
|
28
|
+
- `global: true` is now honored on the `reactive:js` stream path: a single op
|
|
29
|
+
can opt out of the reply's target-root scope to document-wide resolution
|
|
30
|
+
(previously it was silently ignored when a `target` was set).
|
|
31
|
+
|
|
9
32
|
## [0.9.0] - 2026-07-03
|
|
10
33
|
|
|
11
34
|
Consolidates everything since 0.2.6; tags 0.2.7–0.4.8 shipped without changelog
|
data/README.md
CHANGED
|
@@ -354,11 +354,12 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
|
|
|
354
354
|
| `busy_on(:save)` | Mark any element so it carries `data-reactive-busy` **only while `save` is in flight** — a spinner styled with pure CSS, zero Ruby. See [Loading states](#declarative-loading-states-loading--disable_with). |
|
|
355
355
|
| `on(:action, once: true)` | Fire at most once, then unbind (Stimulus's native `:once`). |
|
|
356
356
|
| `on_client(:click, js.toggle("#menu"))` | **Client-only** trigger: applies declared DOM ops with ZERO round trip — no token, no POST, ever. Takes the same `window:`/`once:`/`outside:` modifiers. See [Client-only ops](#client-only-ops-on_client--js--zero-round-trips). |
|
|
357
|
-
| `js` | The immutable op builder behind `on_client`: `show`/`hide`/`toggle` (the `hidden` attribute, with an optional `transition:`), `add_class`/`remove_class`/`toggle_class`, `set_attr`/`remove_attr`/`toggle_attr` (allowlisted names), `focus`/`focus_first`, and `dispatch` — chainable. |
|
|
357
|
+
| `js` | The immutable op builder behind `on_client`: `show`/`hide`/`toggle` (the `hidden` attribute, with an optional `transition:`), `add_class`/`remove_class`/`toggle_class`, `set_attr`/`remove_attr`/`toggle_attr` (allowlisted names), `focus`/`focus_first`, `text` (set `textContent` — XSS-safe), and `dispatch` — chainable. |
|
|
358
358
|
| `reactive_input(:param, **attrs)` / `reactive_select(:param, **attrs)` | Render a control already bound to an action param (no magic `name:`). |
|
|
359
359
|
| `reactive_field(:param, **attrs)` | The attribute hash behind the above — spread onto any control. |
|
|
360
360
|
| `reactive_text(:name, initial)` | Mirror a compute output (or a declared input) into a **text node** — a live preview heading, a character counter, `"Hello, {name}"` — via `textContent` (XSS-safe). The text sibling of `reactive_field`; carries no `name`, so it's never POSTed. See [Client-side computes](#client-side-computes-reactive_compute--reactive_text). |
|
|
361
361
|
| `reactive_compute :name, inputs: { title: :string, qty: :number }, outputs:` | **Typed** inputs: a `:string` reaches the JS reducer raw, a `:number` is coerced through `Number`. The array form (`inputs: %i[a b]`) stays all-numeric. |
|
|
362
|
+
| `reactive_compute :name, ..., mirror: { sum: "#summary-sum" }` | **Cross-root text mirrors**: paint a compute value into declared, id-allowlisted nodes **outside** the reactive root (a recap in another tab pane) via `textContent` — no bespoke listener. See [Cross-root mirrors](#cross-root-mirrors-mirror--painting-a-recap-outside-the-root). |
|
|
362
363
|
| `reactive_root(track_dirty: true, warn_unsaved: true)` / `reactive_field(:param, dirty: true)` | **Dirty tracking** against the DOM's own `defaultValue`/`defaultChecked`/`defaultSelected` — no client state. Marks changed fields + the root `data-reactive-dirty`; `warn_unsaved:` arms a `beforeunload`/`turbo:before-visit` guard. Style with `[data-reactive-dirty]`. See [Dirty-field tracking](#dirty-field-tracking-dirty--track_dirty--warn_unsaved). |
|
|
363
364
|
| `nested_update!(:assoc, attrs)` | Map a nested param onto `<assoc>_attributes` with id preservation; update the record. |
|
|
364
365
|
| `reactive_collection :name, item:, container:, count:, empty:, size:` | Declare an add/remove-row list once; actions call `reply.append`/`prepend`/`remove`. See [Reactive collections](#reactive-collections-addremove-rows--count--empty-state). |
|
|
@@ -771,6 +772,11 @@ button(**on_client(:click, js
|
|
|
771
772
|
- **`focus(to)`** focuses the first match; **`focus_first(to)`** focuses the first
|
|
772
773
|
focusable descendant of the match (e.g. the first menuitem inside an opened
|
|
773
774
|
menu).
|
|
775
|
+
- **`text(to, value)`** sets the target's `textContent` (stringified; `nil`
|
|
776
|
+
clears) — **XSS-safe by construction**, never `innerHTML`, strictly less
|
|
777
|
+
powerful than `set_attr`. With `global: true` it is the **cross-root text
|
|
778
|
+
escape**: paint a value into a recap node outside the component's root
|
|
779
|
+
(`js.text("#sum_total", total, global: true)`).
|
|
774
780
|
- **`dispatch(name, to: nil, detail: {})`** emits a **bubbling `CustomEvent`** so
|
|
775
781
|
another component or a plain Stimulus controller can react to a client-only
|
|
776
782
|
interaction — `to:` picks the element (default: the component root), `detail:`
|
|
@@ -896,6 +902,47 @@ setComputeReducer("preview", ({ title }) => ({
|
|
|
896
902
|
a later morph repaints stale text — the same reconcile contract the whole
|
|
897
903
|
new-vs-persisted split relies on.
|
|
898
904
|
|
|
905
|
+
### Cross-root mirrors (`mirror:`) — painting a recap outside the root
|
|
906
|
+
|
|
907
|
+
`reactive_text` is deliberately **root-isolated** (a nested component's nodes are
|
|
908
|
+
never touched — issue #15's ownership rule). But a derived value often needs to
|
|
909
|
+
show up in a text node that *isn't inside the computing root at all*: a read-only
|
|
910
|
+
recap in another tab pane, a sticky footer total. Collapsing two components into
|
|
911
|
+
one form-wide root just for a display mirror would be a large, risky restructure —
|
|
912
|
+
so the component **declares the escape** instead:
|
|
913
|
+
|
|
914
|
+
```ruby
|
|
915
|
+
reactive_compute :split,
|
|
916
|
+
inputs: %i[a b total],
|
|
917
|
+
outputs: %i[a b],
|
|
918
|
+
mirror: { sum_a: "#sum_a", sum_total: ["#sum_total", "#footer-total"] }
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
On every compute pass, each declared mirror name is painted into its
|
|
922
|
+
document-wide id target(s) via `textContent`. The value comes from wherever the
|
|
923
|
+
pass produced it — one declaration covers all three shapes:
|
|
924
|
+
|
|
925
|
+
- a **reducer-result key** (`sum_total:` above — an *extra*, text-only output the
|
|
926
|
+
reducer returns alongside its real outputs),
|
|
927
|
+
- a **just-written output**'s settled field value,
|
|
928
|
+
- a declared **input**'s identity value (works with **no reducer at all**, like
|
|
929
|
+
the owned-text-node identity mirror).
|
|
930
|
+
|
|
931
|
+
A name the pass produced no value for is **skipped — a mirror never blanks a
|
|
932
|
+
recap**. The security posture matches the rest of the library's default-deny:
|
|
933
|
+
|
|
934
|
+
- **Opt-in and declared, never implicit.** Only the selectors in the `mirror:`
|
|
935
|
+
map are ever written — a plain `reactive_text` node stays root-isolated.
|
|
936
|
+
- **Id selectors only.** A class/attribute/`*`/compound selector **raises at
|
|
937
|
+
declare time**, and the client interpreter re-checks the same shape
|
|
938
|
+
(warn-and-skip) — a hand-built attr can't widen a declared mirror into a
|
|
939
|
+
page-wide selector write.
|
|
940
|
+
- **`textContent` only, never `innerHTML`** — XSS-safe by construction, and never
|
|
941
|
+
a field/attribute/style write.
|
|
942
|
+
|
|
943
|
+
For a **server-driven** cross-root paint (from an action reply or a broadcast),
|
|
944
|
+
use the `text` op instead: `reply.js(js.text("#sum_total", total, global: true))`.
|
|
945
|
+
|
|
899
946
|
### Combobox keyboard navigation (`listnav:`)
|
|
900
947
|
|
|
901
948
|
A searchable list needs Arrow keys to move a highlight, Enter to pick, Escape to
|
|
@@ -1129,6 +1176,9 @@ end
|
|
|
1129
1176
|
`target:` scopes op resolution on the client; it defaults to the bound
|
|
1130
1177
|
component's id for `replace`/`morph`/`update` (so `@root` and component-relative
|
|
1131
1178
|
selectors just work), and to document-scope for a subject-free `reply.with`.
|
|
1179
|
+
`global: true` on a single op opts it out of the target scope to document-wide
|
|
1180
|
+
resolution — `reply.morph.js(js.text("#sum_total", total, global: true))` paints
|
|
1181
|
+
a recap node outside the component while the morph stays root-targeted.
|
|
1132
1182
|
|
|
1133
1183
|
The same ops broadcast to **every** subscriber of a stream over the usual
|
|
1134
1184
|
transport (Action Cable **or** pgbus) — a background nudge to all viewers:
|
|
@@ -399,6 +399,15 @@ const CLIENT_OPS = Object.freeze({
|
|
|
399
399
|
focus: (el) => el.focus?.(),
|
|
400
400
|
focus_first: (el) => firstFocusable(el)?.focus?.(),
|
|
401
401
|
|
|
402
|
+
// Text op (issue #159): set textContent — XSS-safe by construction (never
|
|
403
|
+
// innerHTML), strictly less powerful than set_attr. Change-guarded like
|
|
404
|
+
// #mirrorText. With global: true it is the cross-root text escape: paint a
|
|
405
|
+
// value into a recap node OUTSIDE the component's root.
|
|
406
|
+
text: (el, args) => {
|
|
407
|
+
const text = String(args.value ?? "")
|
|
408
|
+
if (el.textContent !== text) el.textContent = text
|
|
409
|
+
},
|
|
410
|
+
|
|
402
411
|
// Dispatch a bubbling CustomEvent (issue #96). RAW element.dispatchEvent — the
|
|
403
412
|
// controller SHADOWS Stimulus's this.dispatch helper, so it must not be used.
|
|
404
413
|
dispatch: (el, args) => {
|
|
@@ -421,6 +430,19 @@ function guardAttr(name) {
|
|
|
421
430
|
return false
|
|
422
431
|
}
|
|
423
432
|
|
|
433
|
+
// A cross-root mirror target must be a single ID selector (issue #159) — "#" +
|
|
434
|
+
// a CSS identifier, nothing else. The client half of the two-sided default-deny
|
|
435
|
+
// (reactive_compute's `mirror:` validates the SAME shape loudly at declare
|
|
436
|
+
// time): a hand-built mirror attr must not widen a declared text mirror into a
|
|
437
|
+
// page-wide selector write. A refused selector warns + skips (its siblings
|
|
438
|
+
// still apply), matching the attr-allowlist posture.
|
|
439
|
+
const MIRROR_ID_SELECTOR = /^#[A-Za-z_][\w-]*$/
|
|
440
|
+
function guardMirrorSelector(selector) {
|
|
441
|
+
if (typeof selector === "string" && MIRROR_ID_SELECTOR.test(selector)) return true
|
|
442
|
+
console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(selector)} — skipped`)
|
|
443
|
+
return false
|
|
444
|
+
}
|
|
445
|
+
|
|
424
446
|
// The first focusable descendant of `el`, in document order — the natural
|
|
425
447
|
// keyboard target inside an opened menu/dialog. Covers the standard focusable
|
|
426
448
|
// set; :not([tabindex="-1"]) drops explicitly-removed nodes. Returns null when
|
|
@@ -470,12 +492,16 @@ function applyOps(list, resolveTargets) {
|
|
|
470
492
|
// selector with no root (no `target` attr on the stream) resolves document-wide
|
|
471
493
|
// — a broadcast op anchored by a global selector (#bell) rather than a
|
|
472
494
|
// component. Unlike the controller path there is no nested-reactive-root
|
|
473
|
-
// ownership filter: a server-pushed op names its own scope explicitly
|
|
495
|
+
// ownership filter: a server-pushed op names its own scope explicitly —
|
|
496
|
+
// including `global: true`, which opts a single op out of the target-root
|
|
497
|
+
// scope to document-wide resolution (issue #159; the same escape the builder
|
|
498
|
+
// documents for the controller path).
|
|
474
499
|
function streamOpTargets(args, root) {
|
|
475
500
|
const to = args.to
|
|
476
501
|
if (root) {
|
|
477
502
|
if (to === "@root") return [root]
|
|
478
503
|
if (typeof to !== "string" || to === "") return []
|
|
504
|
+
if (args.global) return [...document.querySelectorAll(to)]
|
|
479
505
|
return [...root.querySelectorAll(to)]
|
|
480
506
|
}
|
|
481
507
|
// No target root: document-scoped. "@root" is meaningless here (nothing to
|
|
@@ -759,9 +785,14 @@ export default class extends Controller {
|
|
|
759
785
|
for (const name of inputs) this.#mirrorText(name, ownedField(name)?.value ?? "")
|
|
760
786
|
|
|
761
787
|
const key = this.element.getAttribute("data-reactive-compute-reducer-param")
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
788
|
+
const reduce = key ? computeReducer(key) : null
|
|
789
|
+
if (!reduce) {
|
|
790
|
+
// No reducer registered: the identity pass above still ran, so declared
|
|
791
|
+
// cross-root mirrors of the INPUT names still paint (issue #159) — a
|
|
792
|
+
// reducer-less binding mirrors, exactly like the owned-text-node case.
|
|
793
|
+
this.#applyComputeMirrors({}, ownedField)
|
|
794
|
+
return
|
|
795
|
+
}
|
|
765
796
|
|
|
766
797
|
const outputs = this.#parseComputeList("data-reactive-compute-outputs-param")
|
|
767
798
|
|
|
@@ -808,6 +839,10 @@ export default class extends Controller {
|
|
|
808
839
|
this.#mirrorText(name, result[name])
|
|
809
840
|
}
|
|
810
841
|
}
|
|
842
|
+
|
|
843
|
+
// Cross-root text mirrors (issue #159) — AFTER the outputs are applied, so
|
|
844
|
+
// a mirror keyed on a just-written output paints the settled value.
|
|
845
|
+
this.#applyComputeMirrors(result, ownedField)
|
|
811
846
|
}
|
|
812
847
|
|
|
813
848
|
// Client-side list navigation (combobox keyboard nav, issue #72). Wired by
|
|
@@ -940,6 +975,47 @@ export default class extends Controller {
|
|
|
940
975
|
return Array.from(nodes).filter((el) => this.#ownsField(el))
|
|
941
976
|
}
|
|
942
977
|
|
|
978
|
+
// Cross-root text mirrors (issue #159): paint every DECLARED mirror name into
|
|
979
|
+
// its allowlisted document-wide id targets via textContent — the opt-in escape
|
|
980
|
+
// from root isolation (issue #15) for a recap OUTSIDE the computing root. The
|
|
981
|
+
// value is the reducer's result when it produced one, else the owned field's
|
|
982
|
+
// CURRENT value (an input identity mirror / a just-written output) — one
|
|
983
|
+
// declaration covers all three shapes. A name with NO value this pass is
|
|
984
|
+
// SKIPPED (a mirror never blanks a recap the reducer didn't feed). textContent
|
|
985
|
+
// only (never innerHTML), change-guarded, and NO input dispatch — same
|
|
986
|
+
// contract as #mirrorText. With no mirror declared this is one getAttribute
|
|
987
|
+
// and out — the shipped compute path never touches the document.
|
|
988
|
+
#applyComputeMirrors(result, ownedField) {
|
|
989
|
+
const mirror = this.#parseComputeMirror()
|
|
990
|
+
for (const [name, selectors] of Object.entries(mirror)) {
|
|
991
|
+
const value = name in result ? result[name] : ownedField(name)?.value
|
|
992
|
+
if (value === undefined || value === null) continue
|
|
993
|
+
const text = String(value)
|
|
994
|
+
for (const sel of Array.isArray(selectors) ? selectors : [selectors]) {
|
|
995
|
+
if (!guardMirrorSelector(sel)) continue
|
|
996
|
+
for (const node of document.querySelectorAll(sel)) {
|
|
997
|
+
if (node.textContent === text) continue
|
|
998
|
+
node.textContent = text
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// The declared cross-root mirror map (issue #159): a JSON object of
|
|
1005
|
+
// { name: [id selectors] } from data-reactive-compute-mirror-param (emitted by
|
|
1006
|
+
// reactive_compute's `mirror:`). Absent/malformed degrades to {} — a bad
|
|
1007
|
+
// binding must never throw on input.
|
|
1008
|
+
#parseComputeMirror() {
|
|
1009
|
+
const raw = this.element.getAttribute("data-reactive-compute-mirror-param")
|
|
1010
|
+
if (!raw) return {}
|
|
1011
|
+
try {
|
|
1012
|
+
const parsed = JSON.parse(raw)
|
|
1013
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
|
|
1014
|
+
} catch {
|
|
1015
|
+
return {}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
943
1019
|
// Enqueue the action — debounced if a debounce window is set, else immediately.
|
|
944
1020
|
// Split out of dispatch so both the no-confirm fast path and the post-confirm
|
|
945
1021
|
// microtask share one place (issue #55). `target` is captured up front because
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Controller as B}from"@hotwired/stimulus";import{confirmResolver as T}from"phlex/reactive/confirm";import{computeReducer as w}from"phlex/reactive/compute";function k(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:visit"])return;j["reactive:visit"]=function(){let z=this.getAttribute("data-url");if(z)window.Turbo.visit(z,{action:"advance"})}}function b(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:token"])return;j["reactive:token"]=function(){let z=this.getAttribute("data-reactive-token-value"),Q=this.getAttribute("target");if(!z||!Q)return;let W=document.getElementById(Q);if(W)W.setAttribute("data-reactive-token-value",z)}}function y(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:js"])return;j["reactive:js"]=function(){let z=E(this.getAttribute("data-reactive-ops"));if(!z.length)return;let Q=this.getAttribute("target"),W=Q?document.getElementById(Q):null;if(Q&&!W)return;R(z,(X)=>r(X,W))}}var O=!1;function h(){if(O)return;if(typeof document>"u"||!document.addEventListener)return;O=!0,document.addEventListener("turbo:before-stream-render",v)}function v(j){let z=j.detail,Q=z?.render;if(typeof Q!=="function"||Q.__reactiveDismissWrapped){if(typeof requestAnimationFrame==="function")requestAnimationFrame(N);else setTimeout(N,0);return}let W=async(X)=>{await Q(X),N()};W.__reactiveDismissWrapped=!0,z.render=W}function N(){let j=document.querySelectorAll("[data-reactive-dismiss-after]");for(let z of j){if(z.hasAttribute("data-reactive-dismiss-scheduled"))continue;let Q=Number(z.getAttribute("data-reactive-dismiss-after"));if(!Number.isFinite(Q)||Q<=0)continue;z.setAttribute("data-reactive-dismiss-scheduled",""),setTimeout(()=>z.remove(),Q)}}function e(){O=!1}var D=!1;function f(){if(D)return;if(typeof window>"u"||typeof document>"u")return;if(typeof window.addEventListener!=="function")return;D=!0;let j=()=>{let z=document.documentElement;if(typeof z?.toggleAttribute!=="function")return;z.toggleAttribute("data-reactive-offline",globalThis.navigator?.onLine===!1)};j(),window.addEventListener("online",j),window.addEventListener("offline",j)}function jj(){D=!1}var F="phlex-reactive:latency",I=!1;function u(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(F,String(j))}function p(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(F),I=!1}function m(){if(typeof window>"u"||typeof document>"u")return;if(document.querySelector?.('meta[name="phlex-reactive-env"]')?.content!=="development")return;window.PhlexReactive={enableLatencySim:u,disableLatencySim:p}}function zj(){I=!1}function S(){k(),b(),y(),h(),f(),m()}function c(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)S();else document.addEventListener("turbo:load",S,{once:!0});var L=!1;function g(){if(L)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 Qj(){L=!1}function Wj(){L=!0}if(typeof window<"u"&&typeof document<"u"){let j=()=>setTimeout(g,0);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",j,{once:!0});else j()}var d=new Set(["href","src","srcdoc","action","formaction","xlink:href","style"]);function l(j){let z=String(j).toLowerCase();return z.startsWith("on")||d.has(z)}function s(j,z,Q){let[W,X,Z]=z;j.classList.add(W,X),Q(),requestAnimationFrame(()=>{j.classList.remove(X),j.classList.add(Z)});let $=!1,G=()=>{if($)return;$=!0,j.classList.remove(W,Z)};j.addEventListener("animationend",G,{once:!0}),setTimeout(G,350)}var C=Object.freeze({show:(j,z)=>A(j,!1,z),hide:(j,z)=>A(j,!0,z),toggle:(j,z)=>A(j,!j.hidden,z),add_class:(j,z)=>j.classList.add(...z.classes??[]),remove_class:(j,z)=>j.classList.remove(...z.classes??[]),toggle_class:(j,z)=>(z.classes??[]).forEach((Q)=>j.classList.toggle(Q)),set_attr:(j,z)=>{if(x(z.name))j.setAttribute(z.name,z.value??"")},remove_attr:(j,z)=>{if(x(z.name))j.removeAttribute(z.name)},toggle_attr:(j,z)=>{if(!x(z.name))return;if(j.hasAttribute(z.name))j.removeAttribute(z.name);else j.setAttribute(z.name,"")},focus:(j)=>j.focus?.(),focus_first:(j)=>n(j)?.focus?.(),dispatch:(j,z)=>{j.dispatchEvent(new CustomEvent(z.name,{bubbles:!0,composed:!0,detail:z.detail??{}}))}});function A(j,z,Q){if(Q?.transition)s(j,Q.transition,()=>j.hidden=z);else j.hidden=z}function x(j){if(!l(j))return!0;return console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(j)} — skipped`),!1}var o='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function n(j){return j.querySelectorAll?.(o)?.[0]??null}function E(j){if(Array.isArray(j))return j;if(typeof j!=="string")return[];try{let z=JSON.parse(j);return Array.isArray(z)?z:[]}catch{return[]}}function R(j,z){for(let Q of j){if(!Array.isArray(Q))continue;let[W,X={}]=Q;if(!Object.hasOwn(C,W)){console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(W)} — skipped`);continue}for(let Z of z(X))C[W](Z,X)}}function r(j,z){let Q=j.to;if(z){if(Q==="@root")return[z];if(typeof Q!=="string"||Q==="")return[];return[...z.querySelectorAll(Q)]}if(typeof Q!=="string"||Q===""||Q==="@root")return[];return[...document.querySelectorAll(Q)]}class Xj extends B{static values={token:String};#F;#M=new Map;#$=new Map;#y;#H;#P;#U=0;#G=new Map;#I=new WeakMap;#Y=new Map;#J;#q;#K;connect(){if(L=!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.#h()){if(this.#J=()=>this.#x(),this.element.addEventListener?.("turbo:morph-element",this.#J),this.#x(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#zj()}}#h(){if((this.element.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let j=this.element.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let z of j)if(this.#j(z))return!0;return!1}disconnect(){this.#c(),this.#d(),this.#Qj()}dispatch(j){let{action:z,params:Q,debounce:W,throttle:X,confirm:Z,outside:$,window:G,optimistic:K,loading:M}=j.params;if(!z)return;if($&&this.element.contains(j.target))return;let V=j.currentTarget??j.target;if(!G&&!this.#Gj(K,V))j.preventDefault();if(!Z)return this.#E(V,z,Q,W,X,K,M);Promise.resolve().then(()=>T(Z)).catch(()=>!1).then((J)=>{if(J)this.#E(V,z,Q,W,X,K,M)})}runOps(j){let{ops:z,outside:Q,window:W}=j.params;if(Q&&this.element.contains(j.target))return;if(!W)j.preventDefault();this.#$j(this.#Zj(z))}trackDirty(){this.#x()}recompute(j){let z=this.#f(),Q=z.map(([J])=>J),W=this.#A(),X=new Map,Z=(J)=>{if(X.has(J))return X.get(J);let q=null;for(let _ of this.element.querySelectorAll(`[name="${J}"]`))if(W(_)){q=_;break}return X.set(J,q),q};for(let J of Q)this.#C(J,Z(J)?.value??"");let $=this.element.getAttribute("data-reactive-compute-reducer-param");if(!$)return;let G=w($);if(!G)return;let K=this.#v("data-reactive-compute-outputs-param"),M={};for(let[J,q]of z){let _=Z(J);if(q==="string")M[J]=_?.value??"";else{let H=Number(_?.value);M[J]=Number.isFinite(H)?H:0}}let V=G(M,{changed:this.#u(j,Q)})||{};for(let J of K){if(!(J in V))continue;let q=Z(J);if(q){if(String(V[J])===q.value)continue;q.value=V[J],q.dispatchEvent(new Event("input",{bubbles:!0}))}else this.#C(J,V[J])}}listnavNext(j){this.#S(j,1)}listnavPrev(j){this.#S(j,-1)}listnavPick(j){let z=this.#L(j),Q=z.findIndex((W)=>W.hasAttribute("data-reactive-highlighted"));if(Q<0)return;j.preventDefault(),z[Q].click()}listnavClose(j){for(let z of this.#L(j))z.removeAttribute("data-reactive-highlighted")}#S(j,z){let Q=this.#L(j);if(!Q.length)return;j.preventDefault();let W=Q.findIndex(($)=>$.hasAttribute("data-reactive-highlighted")),X=W<0?z>0?0:Q.length-1:(W+z+Q.length)%Q.length;for(let $ of Q)$.removeAttribute("data-reactive-highlighted");let Z=Q[X];Z.setAttribute("data-reactive-highlighted","true"),Z.scrollIntoView?.({block:"nearest"})}#L(j){let Q=(j?.currentTarget??j?.target??this.element).getAttribute?.("data-reactive-listnav-option-param")??this.element.getAttribute("data-reactive-listnav-option-param");if(!Q)return[];let W=this.#A();return Array.from(this.element.querySelectorAll(Q)).filter(W)}#v(j){let z=this.element.getAttribute(j);if(!z)return[];try{let Q=JSON.parse(z);return Array.isArray(Q)?Q:[]}catch{return[]}}#f(){let j=this.element.getAttribute("data-reactive-compute-inputs-param");if(!j)return[];try{let z=JSON.parse(j);if(Array.isArray(z))return z.map((Q)=>[Q,"number"]);if(z&&typeof z==="object")return Object.entries(z);return[]}catch{return[]}}#u(j,z){let Q=j?.target;if(!Q?.name||typeof Q.closest!=="function")return null;if(!z.includes(Q.name))return null;return this.#j(Q)?Q.name:null}#C(j,z){let Q=String(z);for(let W of this.#p(j)){if(W.textContent===Q)continue;W.textContent=Q}}#p(j){let z=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(z).filter((Q)=>this.#j(Q))}#E(j,z,Q,W,X,Z,$){if(this.#_("reactive:before-dispatch",{action:z,params:this.#k(Q),element:this.element},{cancelable:!0}).defaultPrevented)return;let K=Number(W)||0;if(K>0)return this.#m(j,K,z,Q,Z,$);let M=Number(X)||0;if(M>0)return this.#g(j,M,z,Q,Z,$);return this.#V(z,Q,Z,j,$)}#V(j,z,Q,W,X){let Z=this.#Jj(Q,W),$=this.#Kj(j,W,X);return this.queue=(this.queue??Promise.resolve()).then(()=>this.#i(j,z,Z,$)),this.queue}#m(j,z,Q,W,X,Z){this.#N(j);let $=()=>{this.#N(j),this.#V(Q,W,X,j,Z)},G=setTimeout($,z);j?.addEventListener?.("blur",$,{once:!0}),this.#M.set(j,{timer:G,flush:$})}#N(j){let z=this.#M.get(j);if(!z)return;clearTimeout(z.timer),j?.removeEventListener?.("blur",z.flush),this.#M.delete(j)}#c(){for(let j of[...this.#M.keys()])this.#N(j)}#g(j,z,Q,W,X,Z){let $=this.#$.get(j)??new Map;if($.has(Q))return;let G=setTimeout(()=>{if($.delete(Q),$.size===0)this.#$.delete(j)},z);return $.set(Q,G),this.#$.set(j,$),this.#V(Q,W,X,j,Z)}#d(){for(let j of this.#$.values())for(let z of j.values())clearTimeout(z);this.#$.clear()}#_(j,z,{cancelable:Q=!1}={}){let W=new CustomEvent(j,{bubbles:!0,composed:!0,cancelable:Q,detail:z});return(this.element.isConnected?this.element:document).dispatchEvent(W),W}#z(j,z,Q,W){let X=()=>{if(!this.element.isConnected){console.warn("[phlex-reactive] retry() ignored — the reactive root left the DOM");return}return this.#V(j,z)};this.#_("reactive:error",{action:j,params:Q,...W,retry:X})}#Q(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#l(){this.element?.removeAttribute?.("data-reactive-error")}#s(){let j=document.querySelector("[data-reactive-error-flash]");if(!j?.content)return;let z=j.getAttribute("data-reactive-error-flash")||"flash",Q=document.getElementById(z);if(!Q)return;Q.appendChild(j.content.cloneNode(!0))}#o(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(F));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!I)I=!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((z)=>setTimeout(z,j))}#n(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#R(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#r(j){if(!j)return[];let z=[],Q=/<turbo-stream\b([^>]*)>/g,W;while((W=Q.exec(j))!==null){let X=W[1],Z=X.match(/\baction="([^"]*)"/)?.[1]??"?",$=X.match(/\btarget="([^"]*)"/)?.[1];z.push($?`${Z} → #${$}`:Z)}return z}#a(j){let{action:z,status:Q,ms:W}=j,Z=`reactive ${this.element?.id?`#${this.element.id} `:""}${z} → ${Q??"—"} (${Math.round(W)}ms)`;if(console.groupCollapsed(Z),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#i(j,z,Q,W){let{fields:X,files:Z}=this.#e(),$=this.#k(z),G={...X,...$},K=this.#W,M=Z.length>0,V=M?this.#Wj(K,j,G,Z):JSON.stringify({token:K,act:j,params:G}),J=this.#n()?{action:j,paramNames:Object.keys($),fieldNames:Object.keys(X),encoding:M?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#R()}:null;await this.#o();try{if(navigator.onLine===!1){this.#X(Q),this.#Q("offline"),this.#z(j,z,G,{kind:"offline"});return}let q;try{let Y={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#Lj()};if(!M)Y["Content-Type"]="application/json";let U=this.#Nj();if(U)Y["X-Pgbus-Connection"]=U;q=await fetch(this.#Uj(),{method:"POST",headers:Y,body:V,credentials:"same-origin",signal:AbortSignal.timeout(this.#Ij())})}catch(Y){if(console.error("[phlex-reactive] action error",Y),this.#X(Q),Y?.name==="TimeoutError"||Y?.name==="AbortError"){this.#Q("timeout"),this.#z(j,z,G,{kind:"timeout"});return}this.#s(),this.#Q("network"),this.#z(j,z,G,{kind:"network"});return}if(J)J.status=q.status;if(q.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#X(Q),this.#Q("redirected"),this.#z(j,z,G,{kind:"redirected",status:q.status});return}if(!q.ok){let Y=await q.text();if(console.error(`[phlex-reactive] action failed: HTTP ${q.status}`,Y),this.#X(Q),(q.headers.get("Content-Type")||"").includes("turbo-stream")){let U=this.#T(Y);if(this.#W=U??this.#W,J)this.#B(J,Y,U);window.Turbo.renderStreamMessage(Y)}this.#Q("http"),this.#z(j,z,G,{kind:"http",status:q.status,body:Y});return}let _=q.headers.get("Content-Type")||"";if(!_.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${_}" — no update applied`),this.#X(Q),this.#Q("content-type"),this.#z(j,z,G,{kind:"content-type",status:q.status});return}let H=await q.text(),P=this.#T(H);if(this.#W=P??this.#W,J)this.#B(J,H,P);window.Turbo.renderStreamMessage(H),this.#l(),this.#_("reactive:applied",{action:j,params:G,html:H})}catch(q){console.error("[phlex-reactive] action error",q),this.#X(Q),this.#_("reactive:error",{action:j,params:G,kind:"apply"})}finally{if(W?.(),J)this.#a({...J,ms:this.#R()-J.started})}}#B(j,z,Q){j.streams=this.#r(z),j.tokenRefreshed=Q!=null}get#W(){return this.#F??this.tokenValue}set#W(j){this.#F=j}#T(j){let z=this.element.id;if(!z)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:Q,self:W}=this.#t(z),X=j.match(Q);if(X)return X[1];let Z=j.match(W);if(Z)return Z[1].match(/data-reactive-token-value="([^"]+)"/)?.[1];return}#t(j){let z=this.#P;if(z&&z.id===j)return z;let Q=c(j);return this.#P={id:j,token:new RegExp(`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${Q}"[^>]*\\bdata-reactive-token-value="([^"]+)"`),self:new RegExp(`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${Q}"[^>]*>([\\s\\S]*?)</turbo-stream>`)}}#j(j){return j.closest('[data-controller~="reactive"]')===this.element}#A(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(z)=>this.#j(z)}#e(){let j={},z=[],Q=this.#A();return this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((W)=>{if(!Q(W))return;if(W.type==="file")for(let X of W.files??[])z.push({name:W.name,file:X,multiple:W.multiple});else if(W.type==="checkbox")j[W.name]=W.checked;else if(W.type==="radio"){if(W.checked)j[W.name]=W.value}else j[W.name]=W.value}),this.element.querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])").forEach((W)=>{if(!Q(W))return;let X=W.getAttribute("name");if(!X)return;let Z=j[X];if(Z==null||Z==="")j[X]=W.value??W.textContent??W.innerHTML??""}),{fields:j,files:z}}#x(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((z)=>{if(!this.#j(z))return;if(z.type==="file")return;if(this.#jj(z))z.setAttribute("data-reactive-dirty","true"),j++;else z.removeAttribute("data-reactive-dirty")}),j>0)this.element.setAttribute("data-reactive-dirty",String(j));else this.element.removeAttribute("data-reactive-dirty")}#jj(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((z)=>z.selected!==z.defaultSelected);return j.value!==j.defaultValue}#w(){let j=this.element.getAttribute?.("data-reactive-dirty"),z=Number(j);return Number.isFinite(z)&&z>0?z:0}#zj(){if(typeof window>"u"||typeof window.addEventListener!=="function")return;this.#q=(j)=>{if(this.#w()===0)return;return j.preventDefault(),j.returnValue="You have unsaved changes.",j.returnValue},this.#K=(j)=>{if(this.#w()===0)return;if(!(typeof window.confirm==="function"?window.confirm("You have unsaved changes. Leave anyway?"):!0))j.preventDefault?.()},window.addEventListener("beforeunload",this.#q),window.addEventListener("turbo:before-visit",this.#K)}#Qj(){if(this.#J)this.element.removeEventListener?.("turbo:morph-element",this.#J),this.#J=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#q)window.removeEventListener("beforeunload",this.#q);if(this.#K)window.removeEventListener("turbo:before-visit",this.#K)}this.#q=void 0,this.#K=void 0}#Wj(j,z,Q,W){let X=new FormData;X.append("token",j),X.append("act",z);for(let[$,G]of Object.entries(Q))this.#O(X,`params[${$}]`,G);let Z=this.#Xj(W);for(let{name:$,file:G,multiple:K}of W){let V=K||Z.has($)?`params[${$}][]`:`params[${$}]`;X.append(V,G,G.name)}return X}#O(j,z,Q){if(Q==null)j.append(z,"");else if(Array.isArray(Q))Q.forEach((W,X)=>this.#O(j,`${z}[${X}]`,W));else if(typeof Q==="object")for(let[W,X]of Object.entries(Q))this.#O(j,`${z}[${W}]`,X);else j.append(z,String(Q))}#Xj(j){let z=new Map;for(let{name:Q}of j)z.set(Q,(z.get(Q)??0)+1);return new Set([...z].filter(([,Q])=>Q>1).map(([Q])=>Q))}#k(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#Zj(j){return E(j)}#$j(j){R(j,(z)=>this.#D(z))}#D(j){let z=j.to;if(z==="@root")return[this.element];if(typeof z!=="string"||z==="")return[];if(j.global)return[...document.querySelectorAll(z)];return[...this.element.querySelectorAll(z)].filter((Q)=>this.#j(Q))}#Gj(j,z){if(j?.checked!=="keep")return!1;let Q=z?.type;return Q==="checkbox"||Q==="radio"}#Jj(j,z){if(!j)return null;let Q=this.#qj(j,z),W=[];for(let X of Q){if(j.add_class){let Z=j.add_class.filter(($)=>!X.classList.contains($));if(X.classList.add(...Z),Z.length)W.push(()=>X.classList.remove(...Z))}if(j.remove_class){let Z=j.remove_class.filter(($)=>X.classList.contains($));if(X.classList.remove(...Z),Z.length)W.push(()=>X.classList.add(...Z))}if(j.toggle_class)j.toggle_class.forEach((Z)=>X.classList.toggle(Z)),W.push(()=>j.toggle_class.forEach((Z)=>X.classList.toggle(Z)));if(j.hide)X.hidden=!0,W.push(()=>X.hidden=!1)}if(j.checked==="keep"&&z&&"checked"in z){let X=z.checked;W.push(()=>z.checked=!X)}return W.length?W:null}#X(j){if(!j)return;if(!this.element.isConnected)return;for(let z of j)z()}#qj(j,z){if(j.to==null)return z?[z]:[];return this.#D({to:j.to})}#Kj(j,z,Q){this.#Mj(j,z);let W=this.#Vj(j,z,Q),X=!1;return()=>{if(X)return;X=!0,this.#Yj(j,z),W()}}#Mj(j,z){if(this.#Z(z,j,1),this.#Z(this.element,j,1),this.#G.set(j,(this.#G.get(j)??0)+1),this.#U++===0)this.element.setAttribute("aria-busy","true");for(let Q of this.#b(j))this.#Z(Q,j,1)}#Yj(j,z){this.#Z(z,j,-1),this.#Z(this.element,j,-1);let Q=(this.#G.get(j)??1)-1;if(Q<=0)this.#G.delete(j);else this.#G.set(j,Q);if(--this.#U<=0)this.#U=0,this.element.removeAttribute("aria-busy");for(let W of this.#b(j))this.#Z(W,j,-1)}#Z(j,z,Q){if(!j||typeof j.getAttribute!=="function")return;let W=this.#I.get(j)??new Map,X=(W.get(z)??0)+Q;if(X<=0)W.delete(z);else W.set(z,X);if(W.size===0){this.#I.delete(j),j.removeAttribute("data-reactive-busy");return}this.#I.set(j,W),j.setAttribute("data-reactive-busy",[...W.keys()].join(" "))}#b(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((Q)=>Q.getAttribute("data-reactive-busy-on")===j&&this.#j(Q))}#Vj(j,z,Q){if(!Q||!z)return()=>{};let W=this.#Hj(Q,z),X=Array.isArray(Q.class)?Q.class:[],Z=[];for(let G of W){let K=X.filter((M)=>!G.classList.contains(M));if(G.classList.add(...K),K.length)Z.push([G,K])}let $=this.#Y.get(z);if($)$.count++;else if(Q.disable||Q.text!=null)this.#Y.set(z,{count:1,disabled:z.disabled,text:z.textContent,hadText:Q.text!=null});if(Q.disable)z.disabled=!0;if(Q.text!=null)z.textContent=Q.text;return()=>{for(let[G,K]of Z)if(G.isConnected)G.classList.remove(...K);this.#_j(z,Q)}}#_j(j,z){let Q=this.#Y.get(j);if(!Q)return;if(--Q.count>0)return;if(this.#Y.delete(j),!j.isConnected)return;if(z.disable)j.disabled=Q.disabled;if(Q.hadText&&j.textContent===z.text)j.textContent=Q.text}#Hj(j,z){if(j.to==null)return z?[z]:[];return this.#D({to:j.to})}#Uj(){return this.#y??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#Ij(){if(this.#H!=null)return this.#H;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,z=Number(j);return this.#H=Number.isFinite(z)&&z>0?z:30000}#Lj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}#Nj(){return document.querySelector("pgbus-stream-source[connection-id]")?.getAttribute("connection-id")||document.querySelector('meta[name="pgbus-connection-id"]')?.content||null}}export{k as registerReactiveVisit,b as registerReactiveToken,f as registerReactiveOffline,y as registerReactiveJs,h as registerReactiveDismiss,S as registerReactiveActions,c as escapeRegExp,u as enableLatencySim,p as disableLatencySim,Xj as default,g as checkReactiveRegistration,Qj as __resetReactiveRegistrationForTest,jj as __resetReactiveOfflineForTest,zj as __resetReactiveLatencyForTest,e as __resetReactiveDismissForTest,Wj as __markReactiveConnectedForTest,F as LATENCY_KEY};
|
|
1
|
+
import{Controller as R}from"@hotwired/stimulus";import{confirmResolver as T}from"phlex/reactive/confirm";import{computeReducer as k}from"phlex/reactive/compute";function w(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:visit"])return;j["reactive:visit"]=function(){let z=this.getAttribute("data-url");if(z)window.Turbo.visit(z,{action:"advance"})}}function b(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:token"])return;j["reactive:token"]=function(){let z=this.getAttribute("data-reactive-token-value"),Q=this.getAttribute("target");if(!z||!Q)return;let W=document.getElementById(Q);if(W)W.setAttribute("data-reactive-token-value",z)}}function h(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:js"])return;j["reactive:js"]=function(){let z=C(this.getAttribute("data-reactive-ops"));if(!z.length)return;let Q=this.getAttribute("target"),W=Q?document.getElementById(Q):null;if(Q&&!W)return;B(z,(X)=>i(X,W))}}var O=!1;function y(){if(O)return;if(typeof document>"u"||!document.addEventListener)return;O=!0,document.addEventListener("turbo:before-stream-render",v)}function v(j){let z=j.detail,Q=z?.render;if(typeof Q!=="function"||Q.__reactiveDismissWrapped){if(typeof requestAnimationFrame==="function")requestAnimationFrame(L);else setTimeout(L,0);return}let W=async(X)=>{await Q(X),L()};W.__reactiveDismissWrapped=!0,z.render=W}function L(){let j=document.querySelectorAll("[data-reactive-dismiss-after]");for(let z of j){if(z.hasAttribute("data-reactive-dismiss-scheduled"))continue;let Q=Number(z.getAttribute("data-reactive-dismiss-after"));if(!Number.isFinite(Q)||Q<=0)continue;z.setAttribute("data-reactive-dismiss-scheduled",""),setTimeout(()=>z.remove(),Q)}}function zj(){O=!1}var x=!1;function f(){if(x)return;if(typeof window>"u"||typeof document>"u")return;if(typeof window.addEventListener!=="function")return;x=!0;let j=()=>{let z=document.documentElement;if(typeof z?.toggleAttribute!=="function")return;z.toggleAttribute("data-reactive-offline",globalThis.navigator?.onLine===!1)};j(),window.addEventListener("online",j),window.addEventListener("offline",j)}function Qj(){x=!1}var P="phlex-reactive:latency",I=!1;function u(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(P,String(j))}function p(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(P),I=!1}function m(){if(typeof window>"u"||typeof document>"u")return;if(document.querySelector?.('meta[name="phlex-reactive-env"]')?.content!=="development")return;window.PhlexReactive={enableLatencySim:u,disableLatencySim:p}}function Wj(){I=!1}function S(){w(),b(),h(),y(),f(),m()}function c(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)S();else document.addEventListener("turbo:load",S,{once:!0});var N=!1;function g(){if(N)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 Xj(){N=!1}function Zj(){N=!0}if(typeof window<"u"&&typeof document<"u"){let j=()=>setTimeout(g,0);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",j,{once:!0});else j()}var d=new Set(["href","src","srcdoc","action","formaction","xlink:href","style"]);function l(j){let z=String(j).toLowerCase();return z.startsWith("on")||d.has(z)}function s(j,z,Q){let[W,X,Z]=z;j.classList.add(W,X),Q(),requestAnimationFrame(()=>{j.classList.remove(X),j.classList.add(Z)});let $=!1,G=()=>{if($)return;$=!0,j.classList.remove(W,Z)};j.addEventListener("animationend",G,{once:!0}),setTimeout(G,350)}var E=Object.freeze({show:(j,z)=>A(j,!1,z),hide:(j,z)=>A(j,!0,z),toggle:(j,z)=>A(j,!j.hidden,z),add_class:(j,z)=>j.classList.add(...z.classes??[]),remove_class:(j,z)=>j.classList.remove(...z.classes??[]),toggle_class:(j,z)=>(z.classes??[]).forEach((Q)=>j.classList.toggle(Q)),set_attr:(j,z)=>{if(D(z.name))j.setAttribute(z.name,z.value??"")},remove_attr:(j,z)=>{if(D(z.name))j.removeAttribute(z.name)},toggle_attr:(j,z)=>{if(!D(z.name))return;if(j.hasAttribute(z.name))j.removeAttribute(z.name);else j.setAttribute(z.name,"")},focus:(j)=>j.focus?.(),focus_first:(j)=>r(j)?.focus?.(),text:(j,z)=>{let Q=String(z.value??"");if(j.textContent!==Q)j.textContent=Q},dispatch:(j,z)=>{j.dispatchEvent(new CustomEvent(z.name,{bubbles:!0,composed:!0,detail:z.detail??{}}))}});function A(j,z,Q){if(Q?.transition)s(j,Q.transition,()=>j.hidden=z);else j.hidden=z}function D(j){if(!l(j))return!0;return console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(j)} — skipped`),!1}var o=/^#[A-Za-z_][\w-]*$/;function n(j){if(typeof j==="string"&&o.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(j)} — skipped`),!1}var a='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function r(j){return j.querySelectorAll?.(a)?.[0]??null}function C(j){if(Array.isArray(j))return j;if(typeof j!=="string")return[];try{let z=JSON.parse(j);return Array.isArray(z)?z:[]}catch{return[]}}function B(j,z){for(let Q of j){if(!Array.isArray(Q))continue;let[W,X={}]=Q;if(!Object.hasOwn(E,W)){console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(W)} — skipped`);continue}for(let Z of z(X))E[W](Z,X)}}function i(j,z){let Q=j.to;if(z){if(Q==="@root")return[z];if(typeof Q!=="string"||Q==="")return[];if(j.global)return[...document.querySelectorAll(Q)];return[...z.querySelectorAll(Q)]}if(typeof Q!=="string"||Q===""||Q==="@root")return[];return[...document.querySelectorAll(Q)]}class $j extends R{static values={token:String};#P;#Y=new Map;#$=new Map;#y;#_;#F;#M=0;#G=new Map;#I=new WeakMap;#V=new Map;#J;#q;#K;connect(){if(N=!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.#v()){if(this.#J=()=>this.#D(),this.element.addEventListener?.("turbo:morph-element",this.#J),this.#D(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#Wj()}}#v(){if((this.element.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let j=this.element.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let z of j)if(this.#j(z))return!0;return!1}disconnect(){this.#d(),this.#s(),this.#Xj()}dispatch(j){let{action:z,params:Q,debounce:W,throttle:X,confirm:Z,outside:$,window:G,optimistic:K,loading:Y}=j.params;if(!z)return;if($&&this.element.contains(j.target))return;let V=j.currentTarget??j.target;if(!G&&!this.#qj(K,V))j.preventDefault();if(!Z)return this.#B(V,z,Q,W,X,K,Y);Promise.resolve().then(()=>T(Z)).catch(()=>!1).then((J)=>{if(J)this.#B(V,z,Q,W,X,K,Y)})}runOps(j){let{ops:z,outside:Q,window:W}=j.params;if(Q&&this.element.contains(j.target))return;if(!W)j.preventDefault();this.#Jj(this.#Gj(z))}trackDirty(){this.#D()}recompute(j){let z=this.#u(),Q=z.map(([J])=>J),W=this.#A(),X=new Map,Z=(J)=>{if(X.has(J))return X.get(J);let q=null;for(let U of this.element.querySelectorAll(`[name="${J}"]`))if(W(U)){q=U;break}return X.set(J,q),q};for(let J of Q)this.#E(J,Z(J)?.value??"");let $=this.element.getAttribute("data-reactive-compute-reducer-param"),G=$?k($):null;if(!G){this.#C({},Z);return}let K=this.#f("data-reactive-compute-outputs-param"),Y={};for(let[J,q]of z){let U=Z(J);if(q==="string")Y[J]=U?.value??"";else{let _=Number(U?.value);Y[J]=Number.isFinite(_)?_:0}}let V=G(Y,{changed:this.#p(j,Q)})||{};for(let J of K){if(!(J in V))continue;let q=Z(J);if(q){if(String(V[J])===q.value)continue;q.value=V[J],q.dispatchEvent(new Event("input",{bubbles:!0}))}else this.#E(J,V[J])}this.#C(V,Z)}listnavNext(j){this.#S(j,1)}listnavPrev(j){this.#S(j,-1)}listnavPick(j){let z=this.#N(j),Q=z.findIndex((W)=>W.hasAttribute("data-reactive-highlighted"));if(Q<0)return;j.preventDefault(),z[Q].click()}listnavClose(j){for(let z of this.#N(j))z.removeAttribute("data-reactive-highlighted")}#S(j,z){let Q=this.#N(j);if(!Q.length)return;j.preventDefault();let W=Q.findIndex(($)=>$.hasAttribute("data-reactive-highlighted")),X=W<0?z>0?0:Q.length-1:(W+z+Q.length)%Q.length;for(let $ of Q)$.removeAttribute("data-reactive-highlighted");let Z=Q[X];Z.setAttribute("data-reactive-highlighted","true"),Z.scrollIntoView?.({block:"nearest"})}#N(j){let Q=(j?.currentTarget??j?.target??this.element).getAttribute?.("data-reactive-listnav-option-param")??this.element.getAttribute("data-reactive-listnav-option-param");if(!Q)return[];let W=this.#A();return Array.from(this.element.querySelectorAll(Q)).filter(W)}#f(j){let z=this.element.getAttribute(j);if(!z)return[];try{let Q=JSON.parse(z);return Array.isArray(Q)?Q:[]}catch{return[]}}#u(){let j=this.element.getAttribute("data-reactive-compute-inputs-param");if(!j)return[];try{let z=JSON.parse(j);if(Array.isArray(z))return z.map((Q)=>[Q,"number"]);if(z&&typeof z==="object")return Object.entries(z);return[]}catch{return[]}}#p(j,z){let Q=j?.target;if(!Q?.name||typeof Q.closest!=="function")return null;if(!z.includes(Q.name))return null;return this.#j(Q)?Q.name:null}#E(j,z){let Q=String(z);for(let W of this.#m(j)){if(W.textContent===Q)continue;W.textContent=Q}}#m(j){let z=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(z).filter((Q)=>this.#j(Q))}#C(j,z){let Q=this.#c();for(let[W,X]of Object.entries(Q)){let Z=W in j?j[W]:z(W)?.value;if(Z===void 0||Z===null)continue;let $=String(Z);for(let G of Array.isArray(X)?X:[X]){if(!n(G))continue;for(let K of document.querySelectorAll(G)){if(K.textContent===$)continue;K.textContent=$}}}}#c(){let j=this.element.getAttribute("data-reactive-compute-mirror-param");if(!j)return{};try{let z=JSON.parse(j);return z&&typeof z==="object"&&!Array.isArray(z)?z:{}}catch{return{}}}#B(j,z,Q,W,X,Z,$){if(this.#U("reactive:before-dispatch",{action:z,params:this.#b(Q),element:this.element},{cancelable:!0}).defaultPrevented)return;let K=Number(W)||0;if(K>0)return this.#g(j,K,z,Q,Z,$);let Y=Number(X)||0;if(Y>0)return this.#l(j,Y,z,Q,Z,$);return this.#H(z,Q,Z,j,$)}#H(j,z,Q,W,X){let Z=this.#Kj(Q,W),$=this.#Vj(j,W,X);return this.queue=(this.queue??Promise.resolve()).then(()=>this.#e(j,z,Z,$)),this.queue}#g(j,z,Q,W,X,Z){this.#L(j);let $=()=>{this.#L(j),this.#H(Q,W,X,j,Z)},G=setTimeout($,z);j?.addEventListener?.("blur",$,{once:!0}),this.#Y.set(j,{timer:G,flush:$})}#L(j){let z=this.#Y.get(j);if(!z)return;clearTimeout(z.timer),j?.removeEventListener?.("blur",z.flush),this.#Y.delete(j)}#d(){for(let j of[...this.#Y.keys()])this.#L(j)}#l(j,z,Q,W,X,Z){let $=this.#$.get(j)??new Map;if($.has(Q))return;let G=setTimeout(()=>{if($.delete(Q),$.size===0)this.#$.delete(j)},z);return $.set(Q,G),this.#$.set(j,$),this.#H(Q,W,X,j,Z)}#s(){for(let j of this.#$.values())for(let z of j.values())clearTimeout(z);this.#$.clear()}#U(j,z,{cancelable:Q=!1}={}){let W=new CustomEvent(j,{bubbles:!0,composed:!0,cancelable:Q,detail:z});return(this.element.isConnected?this.element:document).dispatchEvent(W),W}#z(j,z,Q,W){let X=()=>{if(!this.element.isConnected){console.warn("[phlex-reactive] retry() ignored — the reactive root left the DOM");return}return this.#H(j,z)};this.#U("reactive:error",{action:j,params:Q,...W,retry:X})}#Q(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#o(){this.element?.removeAttribute?.("data-reactive-error")}#n(){let j=document.querySelector("[data-reactive-error-flash]");if(!j?.content)return;let z=j.getAttribute("data-reactive-error-flash")||"flash",Q=document.getElementById(z);if(!Q)return;Q.appendChild(j.content.cloneNode(!0))}#a(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(P));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!I)I=!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((z)=>setTimeout(z,j))}#r(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#R(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#i(j){if(!j)return[];let z=[],Q=/<turbo-stream\b([^>]*)>/g,W;while((W=Q.exec(j))!==null){let X=W[1],Z=X.match(/\baction="([^"]*)"/)?.[1]??"?",$=X.match(/\btarget="([^"]*)"/)?.[1];z.push($?`${Z} → #${$}`:Z)}return z}#t(j){let{action:z,status:Q,ms:W}=j,Z=`reactive ${this.element?.id?`#${this.element.id} `:""}${z} → ${Q??"—"} (${Math.round(W)}ms)`;if(console.groupCollapsed(Z),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#e(j,z,Q,W){let{fields:X,files:Z}=this.#zj(),$=this.#b(z),G={...X,...$},K=this.#W,Y=Z.length>0,V=Y?this.#Zj(K,j,G,Z):JSON.stringify({token:K,act:j,params:G}),J=this.#r()?{action:j,paramNames:Object.keys($),fieldNames:Object.keys(X),encoding:Y?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#R()}:null;await this.#a();try{if(navigator.onLine===!1){this.#X(Q),this.#Q("offline"),this.#z(j,z,G,{kind:"offline"});return}let q;try{let H={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#Aj()};if(!Y)H["Content-Type"]="application/json";let M=this.#Dj();if(M)H["X-Pgbus-Connection"]=M;q=await fetch(this.#Nj(),{method:"POST",headers:H,body:V,credentials:"same-origin",signal:AbortSignal.timeout(this.#Lj())})}catch(H){if(console.error("[phlex-reactive] action error",H),this.#X(Q),H?.name==="TimeoutError"||H?.name==="AbortError"){this.#Q("timeout"),this.#z(j,z,G,{kind:"timeout"});return}this.#n(),this.#Q("network"),this.#z(j,z,G,{kind:"network"});return}if(J)J.status=q.status;if(q.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#X(Q),this.#Q("redirected"),this.#z(j,z,G,{kind:"redirected",status:q.status});return}if(!q.ok){let H=await q.text();if(console.error(`[phlex-reactive] action failed: HTTP ${q.status}`,H),this.#X(Q),(q.headers.get("Content-Type")||"").includes("turbo-stream")){let M=this.#k(H);if(this.#W=M??this.#W,J)this.#T(J,H,M);window.Turbo.renderStreamMessage(H)}this.#Q("http"),this.#z(j,z,G,{kind:"http",status:q.status,body:H});return}let U=q.headers.get("Content-Type")||"";if(!U.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${U}" — no update applied`),this.#X(Q),this.#Q("content-type"),this.#z(j,z,G,{kind:"content-type",status:q.status});return}let _=await q.text(),F=this.#k(_);if(this.#W=F??this.#W,J)this.#T(J,_,F);window.Turbo.renderStreamMessage(_),this.#o(),this.#U("reactive:applied",{action:j,params:G,html:_})}catch(q){console.error("[phlex-reactive] action error",q),this.#X(Q),this.#U("reactive:error",{action:j,params:G,kind:"apply"})}finally{if(W?.(),J)this.#t({...J,ms:this.#R()-J.started})}}#T(j,z,Q){j.streams=this.#i(z),j.tokenRefreshed=Q!=null}get#W(){return this.#P??this.tokenValue}set#W(j){this.#P=j}#k(j){let z=this.element.id;if(!z)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:Q,self:W}=this.#jj(z),X=j.match(Q);if(X)return X[1];let Z=j.match(W);if(Z)return Z[1].match(/data-reactive-token-value="([^"]+)"/)?.[1];return}#jj(j){let z=this.#F;if(z&&z.id===j)return z;let Q=c(j);return this.#F={id:j,token:new RegExp(`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${Q}"[^>]*\\bdata-reactive-token-value="([^"]+)"`),self:new RegExp(`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${Q}"[^>]*>([\\s\\S]*?)</turbo-stream>`)}}#j(j){return j.closest('[data-controller~="reactive"]')===this.element}#A(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(z)=>this.#j(z)}#zj(){let j={},z=[],Q=this.#A();return this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((W)=>{if(!Q(W))return;if(W.type==="file")for(let X of W.files??[])z.push({name:W.name,file:X,multiple:W.multiple});else if(W.type==="checkbox")j[W.name]=W.checked;else if(W.type==="radio"){if(W.checked)j[W.name]=W.value}else j[W.name]=W.value}),this.element.querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])").forEach((W)=>{if(!Q(W))return;let X=W.getAttribute("name");if(!X)return;let Z=j[X];if(Z==null||Z==="")j[X]=W.value??W.textContent??W.innerHTML??""}),{fields:j,files:z}}#D(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((z)=>{if(!this.#j(z))return;if(z.type==="file")return;if(this.#Qj(z))z.setAttribute("data-reactive-dirty","true"),j++;else z.removeAttribute("data-reactive-dirty")}),j>0)this.element.setAttribute("data-reactive-dirty",String(j));else this.element.removeAttribute("data-reactive-dirty")}#Qj(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((z)=>z.selected!==z.defaultSelected);return j.value!==j.defaultValue}#w(){let j=this.element.getAttribute?.("data-reactive-dirty"),z=Number(j);return Number.isFinite(z)&&z>0?z:0}#Wj(){if(typeof window>"u"||typeof window.addEventListener!=="function")return;this.#q=(j)=>{if(this.#w()===0)return;return j.preventDefault(),j.returnValue="You have unsaved changes.",j.returnValue},this.#K=(j)=>{if(this.#w()===0)return;if(!(typeof window.confirm==="function"?window.confirm("You have unsaved changes. Leave anyway?"):!0))j.preventDefault?.()},window.addEventListener("beforeunload",this.#q),window.addEventListener("turbo:before-visit",this.#K)}#Xj(){if(this.#J)this.element.removeEventListener?.("turbo:morph-element",this.#J),this.#J=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#q)window.removeEventListener("beforeunload",this.#q);if(this.#K)window.removeEventListener("turbo:before-visit",this.#K)}this.#q=void 0,this.#K=void 0}#Zj(j,z,Q,W){let X=new FormData;X.append("token",j),X.append("act",z);for(let[$,G]of Object.entries(Q))this.#O(X,`params[${$}]`,G);let Z=this.#$j(W);for(let{name:$,file:G,multiple:K}of W){let V=K||Z.has($)?`params[${$}][]`:`params[${$}]`;X.append(V,G,G.name)}return X}#O(j,z,Q){if(Q==null)j.append(z,"");else if(Array.isArray(Q))Q.forEach((W,X)=>this.#O(j,`${z}[${X}]`,W));else if(typeof Q==="object")for(let[W,X]of Object.entries(Q))this.#O(j,`${z}[${W}]`,X);else j.append(z,String(Q))}#$j(j){let z=new Map;for(let{name:Q}of j)z.set(Q,(z.get(Q)??0)+1);return new Set([...z].filter(([,Q])=>Q>1).map(([Q])=>Q))}#b(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#Gj(j){return C(j)}#Jj(j){B(j,(z)=>this.#x(z))}#x(j){let z=j.to;if(z==="@root")return[this.element];if(typeof z!=="string"||z==="")return[];if(j.global)return[...document.querySelectorAll(z)];return[...this.element.querySelectorAll(z)].filter((Q)=>this.#j(Q))}#qj(j,z){if(j?.checked!=="keep")return!1;let Q=z?.type;return Q==="checkbox"||Q==="radio"}#Kj(j,z){if(!j)return null;let Q=this.#Yj(j,z),W=[];for(let X of Q){if(j.add_class){let Z=j.add_class.filter(($)=>!X.classList.contains($));if(X.classList.add(...Z),Z.length)W.push(()=>X.classList.remove(...Z))}if(j.remove_class){let Z=j.remove_class.filter(($)=>X.classList.contains($));if(X.classList.remove(...Z),Z.length)W.push(()=>X.classList.add(...Z))}if(j.toggle_class)j.toggle_class.forEach((Z)=>X.classList.toggle(Z)),W.push(()=>j.toggle_class.forEach((Z)=>X.classList.toggle(Z)));if(j.hide)X.hidden=!0,W.push(()=>X.hidden=!1)}if(j.checked==="keep"&&z&&"checked"in z){let X=z.checked;W.push(()=>z.checked=!X)}return W.length?W:null}#X(j){if(!j)return;if(!this.element.isConnected)return;for(let z of j)z()}#Yj(j,z){if(j.to==null)return z?[z]:[];return this.#x({to:j.to})}#Vj(j,z,Q){this.#Hj(j,z);let W=this.#_j(j,z,Q),X=!1;return()=>{if(X)return;X=!0,this.#Uj(j,z),W()}}#Hj(j,z){if(this.#Z(z,j,1),this.#Z(this.element,j,1),this.#G.set(j,(this.#G.get(j)??0)+1),this.#M++===0)this.element.setAttribute("aria-busy","true");for(let Q of this.#h(j))this.#Z(Q,j,1)}#Uj(j,z){this.#Z(z,j,-1),this.#Z(this.element,j,-1);let Q=(this.#G.get(j)??1)-1;if(Q<=0)this.#G.delete(j);else this.#G.set(j,Q);if(--this.#M<=0)this.#M=0,this.element.removeAttribute("aria-busy");for(let W of this.#h(j))this.#Z(W,j,-1)}#Z(j,z,Q){if(!j||typeof j.getAttribute!=="function")return;let W=this.#I.get(j)??new Map,X=(W.get(z)??0)+Q;if(X<=0)W.delete(z);else W.set(z,X);if(W.size===0){this.#I.delete(j),j.removeAttribute("data-reactive-busy");return}this.#I.set(j,W),j.setAttribute("data-reactive-busy",[...W.keys()].join(" "))}#h(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((Q)=>Q.getAttribute("data-reactive-busy-on")===j&&this.#j(Q))}#_j(j,z,Q){if(!Q||!z)return()=>{};let W=this.#Ij(Q,z),X=Array.isArray(Q.class)?Q.class:[],Z=[];for(let G of W){let K=X.filter((Y)=>!G.classList.contains(Y));if(G.classList.add(...K),K.length)Z.push([G,K])}let $=this.#V.get(z);if($)$.count++;else if(Q.disable||Q.text!=null)this.#V.set(z,{count:1,disabled:z.disabled,text:z.textContent,hadText:Q.text!=null});if(Q.disable)z.disabled=!0;if(Q.text!=null)z.textContent=Q.text;return()=>{for(let[G,K]of Z)if(G.isConnected)G.classList.remove(...K);this.#Mj(z,Q)}}#Mj(j,z){let Q=this.#V.get(j);if(!Q)return;if(--Q.count>0)return;if(this.#V.delete(j),!j.isConnected)return;if(z.disable)j.disabled=Q.disabled;if(Q.hadText&&j.textContent===z.text)j.textContent=Q.text}#Ij(j,z){if(j.to==null)return z?[z]:[];return this.#x({to:j.to})}#Nj(){return this.#y??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#Lj(){if(this.#_!=null)return this.#_;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,z=Number(j);return this.#_=Number.isFinite(z)&&z>0?z:30000}#Aj(){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{w as registerReactiveVisit,b as registerReactiveToken,f as registerReactiveOffline,h as registerReactiveJs,y as registerReactiveDismiss,S as registerReactiveActions,c as escapeRegExp,u as enableLatencySim,p as disableLatencySim,$j as default,g as checkReactiveRegistration,Xj as __resetReactiveRegistrationForTest,Qj as __resetReactiveOfflineForTest,Wj as __resetReactiveLatencyForTest,zj as __resetReactiveDismissForTest,Zj as __markReactiveConnectedForTest,P as LATENCY_KEY};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=9EC165A88E302A8264756E2164756E21
|
|
4
4
|
//# sourceMappingURL=reactive_controller.min.js.map
|