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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b8404995a25e3917e5785b4e7becaceff75359c5577dd82f8b44e8dd95e2049
4
- data.tar.gz: f1a6e61d8d840aab2273f95405fc19e3e58295c54d2bf10e40ef21bc9bef2b75
3
+ metadata.gz: 5f48d899c85c31d10e7b864e0b4bd290258c3ccbcb4f97fa6052979bba3b68b1
4
+ data.tar.gz: 39b93e42a39480822ec472e9211e5333ca448cec767d38f6c43569c8edcb09fe
5
5
  SHA512:
6
- metadata.gz: 4218da118f87bb6319b54ec8583437f5d208305d936eedbd81b9eaf01ff17941f5a8b7baac98ef141b29ace3b6dbae937b0cee10368b482b2fef2ba9268c49f4
7
- data.tar.gz: ead67e058d25a3443286e70f9a245746604cc584de2a0160446e56a306a99c5f95f8ec0536ca0ec5a1fd1205a3c63ecf27d741e3f8eb992396155c06a84bdb58
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
- if (!key) return
763
- const reduce = computeReducer(key)
764
- if (!reduce) return
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=34DC09D454B3A5E264756E2164756E21
3
+ //# debugId=9EC165A88E302A8264756E2164756E21
4
4
  //# sourceMappingURL=reactive_controller.min.js.map