phlex-reactive 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +52 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +121 -0
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/helpers.rb +55 -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: 86eeeabfa81b914dae1f3598e3c879892478c712afc5631b500b96cbd9b83757
|
|
4
|
+
data.tar.gz: d72882a3b92db0f2d542b5ef457a13f9e74fda538cb3ee5db7d2ac5916833515
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 884a4c34c7d26930bea2873bae0cf3a1ec0ff82d796179b01fe493187f8001d36283ea1e22daeaa347ab72ca8e74be31773cbba77bf0db41bb38781eb578c340
|
|
7
|
+
data.tar.gz: 4b75f6edec65f1d84954260305d384ad6674e21ee3fc5f51270e7055e7679a7d1bb209783d6678b2c506259bf6d1688c6a67c991cc013b22a6f66961bfec84f8
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- **Value-conditional visibility — `reactive_show` (#161).** The `x-show` /
|
|
12
|
+
`data-show` / `wire:show` case — show/hide an element from a form field's
|
|
13
|
+
**current value** — no longer needs a hand-written `change`-listener Stimulus
|
|
14
|
+
controller. Spread `reactive_show(:mode, not: "off")` onto the element to
|
|
15
|
+
show/hide (also `equals:` and `in: [...]`; `equals: true` reads a checkbox's
|
|
16
|
+
checked state, a radio group reads the checked radio's value) and the generic
|
|
17
|
+
controller toggles the `hidden` attribute on every `input`/`change` —
|
|
18
|
+
client-only, zero round trip, no token. The predicate is a **declared literal
|
|
19
|
+
match**, never an expression (no eval surface); exactly one predicate is
|
|
20
|
+
enforced loudly at render; a missing field or malformed wire attr is
|
|
21
|
+
warn-and-skipped (client-side default-deny). Visibility seeds at connect and
|
|
22
|
+
re-syncs after a `turbo:morph-element`; a `reactive_compute` output write
|
|
23
|
+
dispatches a real `input` event, so derived values drive visibility too.
|
|
24
|
+
Ownership follows the nested-root rules (#15). Roots without a binding pay
|
|
25
|
+
one connect-time probe — no new listeners, byte-identical wire.
|
|
26
|
+
|
|
11
27
|
- **Cross-root text mirrors + the `text` client op (#159).** A derived value can
|
|
12
28
|
now be painted into a text node **outside** the computing component's reactive
|
|
13
29
|
root — the read-only recap in another tab pane that previously forced a
|
data/README.md
CHANGED
|
@@ -358,6 +358,7 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
|
|
|
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
|
+
| `reactive_show(:field, equals:/not:/in:)` | **Value-conditional visibility** (the `x-show`/`data-show` case): spread onto the element to show/hide — it toggles `hidden` from the named field's **current value**, client-only, zero round trip. One literal predicate: `equals:`, `not:`, or `in: [...]`; `equals: true` reads a checkbox's checked state. See [Value-conditional visibility](#value-conditional-visibility-reactive_show). |
|
|
361
362
|
| `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
363
|
| `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). |
|
|
363
364
|
| `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). |
|
|
@@ -855,6 +856,57 @@ free as an ordinary action-param name (`on(:switch, key: "pgbus")` still passes
|
|
|
855
856
|
> trigger to its own element (the field saves on Enter; a Cancel button — or the
|
|
856
857
|
> field's own blur — handles Escape), as above.
|
|
857
858
|
|
|
859
|
+
### Value-conditional visibility (`reactive_show`)
|
|
860
|
+
|
|
861
|
+
`on_client` covers the *unconditional* client-only interactions; the last gap
|
|
862
|
+
was **show/hide from a form field's current value** — the Alpine `x-show` /
|
|
863
|
+
Datastar `data-show` / Livewire `wire:show` case ("reveal the details panel
|
|
864
|
+
*while* this select isn't `none`"). That used to force a hand-written
|
|
865
|
+
`change`-listener Stimulus controller back into an otherwise declarative form.
|
|
866
|
+
`reactive_show` closes it: spread it onto the element to show/hide, name the
|
|
867
|
+
controlling field, declare **one literal predicate** — the generic controller
|
|
868
|
+
toggles the `hidden` attribute from the field's current value on every
|
|
869
|
+
`input`/`change`. Client-only, **no token, no POST, ever**:
|
|
870
|
+
|
|
871
|
+
```ruby
|
|
872
|
+
def view_template
|
|
873
|
+
div(**reactive_root) do
|
|
874
|
+
select(name: "mode") { shipping_options }
|
|
875
|
+
div(**reactive_show(:mode, not: "off", hidden: @order.mode == "off")) { shipping_details }
|
|
876
|
+
|
|
877
|
+
input(type: "checkbox", name: "gift")
|
|
878
|
+
div(**reactive_show(:gift, equals: true, hidden: true)) { gift_message_field }
|
|
879
|
+
|
|
880
|
+
select(name: "size") { size_options }
|
|
881
|
+
div(**reactive_show(:size, in: %w[l xl], hidden: true)) { surcharge_note }
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
- **One predicate per binding** — `equals:`, `not:`, or `in:` (a list); zero or
|
|
887
|
+
two raise at render (a dead binding must not silently no-op). Values are
|
|
888
|
+
**stringified literals** matched against the field's value — never an
|
|
889
|
+
expression, so there is no eval surface.
|
|
890
|
+
- **Field reads follow the collection rules**: a checkbox compares its
|
|
891
|
+
*checked* state as `"true"`/`"false"` (so `equals: true` is the checkbox
|
|
892
|
+
form — its `.value` is the constant `"on"`, and it wins over the hidden
|
|
893
|
+
input Rails pairs with it); a radio group reads the **checked** radio's
|
|
894
|
+
value (`""` when none is); everything else reads `.value`. Ownership is the
|
|
895
|
+
usual rule — a nested reactive component's fields and bindings belong to the
|
|
896
|
+
nested component.
|
|
897
|
+
- **Initial state**: the client seeds visibility at connect and reconciles
|
|
898
|
+
after a morph, but render the initial `hidden:` yourself (from the same
|
|
899
|
+
server state that renders the field, as above) so the first paint doesn't
|
|
900
|
+
flash.
|
|
901
|
+
- **Extra attrs ride through the helper** (`reactive_show(:mode, not: "off",
|
|
902
|
+
class: "panel", data: { testid: "details" })`) and deep-merge — a bare
|
|
903
|
+
`data:` spread *beside* it would clobber the binding, same as `on(...)`.
|
|
904
|
+
- **Composes with computes**: a `reactive_compute` output write dispatches a
|
|
905
|
+
real `input` event, so a *derived* value can drive visibility too.
|
|
906
|
+
- Presentational only, strictly weaker than the js ops: it reads an owned
|
|
907
|
+
field and toggles `hidden` on an owned element — no `innerHTML`, no
|
|
908
|
+
attribute freedom, no cross-root writes.
|
|
909
|
+
|
|
858
910
|
### Client-side computes (`reactive_compute` + `reactive_text`)
|
|
859
911
|
|
|
860
912
|
Some math should feel instant with **no round trip** — a NEW, unsaved record's
|
|
@@ -443,6 +443,36 @@ function guardMirrorSelector(selector) {
|
|
|
443
443
|
return false
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
// Evaluate a show binding's declared literal predicate (issue #161) against
|
|
447
|
+
// the controlling field's current value. Exactly one of the three predicate
|
|
448
|
+
// attrs decides: equals (value === literal), not (value !== literal), in
|
|
449
|
+
// (value ∈ a JSON string list). The vocabulary is fixed and literal-only —
|
|
450
|
+
// never an expression, so there is no eval surface (the reactive_show helper
|
|
451
|
+
// enforces the same shape loudly at render; this is the client half of the
|
|
452
|
+
// two-sided posture). Returns true/false for a decidable binding, or null for
|
|
453
|
+
// a malformed/missing predicate — the caller SKIPS a null so a hand-built or
|
|
454
|
+
// stale binding never flips visibility it doesn't understand (default-deny,
|
|
455
|
+
// like the op whitelist).
|
|
456
|
+
function showBindingMatches(el, value) {
|
|
457
|
+
const equals = el.getAttribute("data-reactive-show-equals")
|
|
458
|
+
if (equals !== null) return value === equals
|
|
459
|
+
const not = el.getAttribute("data-reactive-show-not")
|
|
460
|
+
if (not !== null) return value !== not
|
|
461
|
+
const inRaw = el.getAttribute("data-reactive-show-in")
|
|
462
|
+
if (inRaw !== null) {
|
|
463
|
+
try {
|
|
464
|
+
const list = JSON.parse(inRaw)
|
|
465
|
+
if (Array.isArray(list)) return list.includes(value)
|
|
466
|
+
} catch {
|
|
467
|
+
// fall through to the warn below — malformed JSON and a non-array both skip
|
|
468
|
+
}
|
|
469
|
+
console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify(inRaw)} — skipped`)
|
|
470
|
+
return null
|
|
471
|
+
}
|
|
472
|
+
console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped")
|
|
473
|
+
return null
|
|
474
|
+
}
|
|
475
|
+
|
|
446
476
|
// The first focusable descendant of `el`, in document order — the natural
|
|
447
477
|
// keyboard target inside an opened menu/dialog. Covers the standard focusable
|
|
448
478
|
// set; :not([tabindex="-1"]) drops explicitly-removed nodes. Returns null when
|
|
@@ -535,6 +565,9 @@ export default class extends Controller {
|
|
|
535
565
|
#boundScanDirty
|
|
536
566
|
#boundBeforeUnload
|
|
537
567
|
#boundBeforeVisit
|
|
568
|
+
// Show bindings (issue #161): the ONE delegated sync handler shared by the
|
|
569
|
+
// root's input/change/turbo:morph-element listeners, held for teardown.
|
|
570
|
+
#boundSyncShow
|
|
538
571
|
|
|
539
572
|
// Mark that a reactive controller actually connected, so the registration
|
|
540
573
|
// guard above knows the controller was registered (issue #26 part 2).
|
|
@@ -581,6 +614,23 @@ export default class extends Controller {
|
|
|
581
614
|
this.#armUnsavedGuard()
|
|
582
615
|
}
|
|
583
616
|
}
|
|
617
|
+
|
|
618
|
+
// Show bindings (issue #161) — ONLY when this root owns one, so a component
|
|
619
|
+
// without any pays a single probe (the dirty-tracking gate precedent). ONE
|
|
620
|
+
// delegated listener pair on the root (input + change bubble from every
|
|
621
|
+
// owned field — no per-field wiring, and a reactive_compute output write
|
|
622
|
+
// dispatches a real input event, so computed values drive visibility too).
|
|
623
|
+
// The connect sync seeds the initial state — a plain replace re-connects —
|
|
624
|
+
// and turbo:morph-element re-syncs after an in-place morph (which keeps the
|
|
625
|
+
// element connected, fires no Stimulus lifecycle, and may preserve a
|
|
626
|
+
// user-edited field value the server's hidden attrs don't reflect).
|
|
627
|
+
if (this.#showSyncEnabled()) {
|
|
628
|
+
this.#boundSyncShow = () => this.#syncShow()
|
|
629
|
+
this.element.addEventListener?.("input", this.#boundSyncShow)
|
|
630
|
+
this.element.addEventListener?.("change", this.#boundSyncShow)
|
|
631
|
+
this.element.addEventListener?.("turbo:morph-element", this.#boundSyncShow)
|
|
632
|
+
this.#syncShow()
|
|
633
|
+
}
|
|
584
634
|
}
|
|
585
635
|
|
|
586
636
|
// Whether this root opts into dirty tracking (issue #103): track_dirty: puts the
|
|
@@ -605,6 +655,7 @@ export default class extends Controller {
|
|
|
605
655
|
this.#clearAllDebounces()
|
|
606
656
|
this.#clearAllThrottles()
|
|
607
657
|
this.#teardownDirtyTracking()
|
|
658
|
+
this.#teardownShowSync()
|
|
608
659
|
}
|
|
609
660
|
|
|
610
661
|
// Serialize requests per component. Each round trip rewrites the signed
|
|
@@ -1773,6 +1824,76 @@ export default class extends Controller {
|
|
|
1773
1824
|
this.#boundBeforeVisit = undefined
|
|
1774
1825
|
}
|
|
1775
1826
|
|
|
1827
|
+
// Whether this root owns a show binding (issue #161) — the connect() gate, so
|
|
1828
|
+
// a component without one pays only this probe (the #dirtyTrackingEnabled
|
|
1829
|
+
// precedent). A NESTED root's bindings don't count: its own controller
|
|
1830
|
+
// instance syncs them (issue #15 ownership).
|
|
1831
|
+
#showSyncEnabled() {
|
|
1832
|
+
const nodes = this.element.querySelectorAll?.("[data-reactive-show-field]") ?? []
|
|
1833
|
+
for (const el of nodes) if (this.#ownsField(el)) return true
|
|
1834
|
+
return false
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Re-evaluate every OWNED show binding in one pass (issue #161): read the
|
|
1838
|
+
// controlling field's current value, evaluate the declared literal predicate,
|
|
1839
|
+
// toggle `hidden`. A full pass (not per-target) for the same reason as
|
|
1840
|
+
// #scanDirty — a radio group's deselected radio fires no event — and because
|
|
1841
|
+
// several bindings can hang off one field (the value read is memoized per
|
|
1842
|
+
// pass). A binding whose field can't be resolved, or whose predicate is
|
|
1843
|
+
// malformed, leaves visibility ALONE — a bad binding must never break or
|
|
1844
|
+
// blank the page (client-side default-deny).
|
|
1845
|
+
#syncShow() {
|
|
1846
|
+
if (typeof this.element?.querySelectorAll !== "function") return
|
|
1847
|
+
|
|
1848
|
+
const owns = this.#ownershipFilter()
|
|
1849
|
+
const values = new Map()
|
|
1850
|
+
for (const el of this.element.querySelectorAll("[data-reactive-show-field]")) {
|
|
1851
|
+
if (!owns(el)) continue // a nested root's binding is its own controller's job
|
|
1852
|
+
const name = el.getAttribute("data-reactive-show-field")
|
|
1853
|
+
if (!name) continue
|
|
1854
|
+
if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns))
|
|
1855
|
+
const value = values.get(name)
|
|
1856
|
+
if (value === null) continue // no owned field with that name — leave it be
|
|
1857
|
+
const match = showBindingMatches(el, value)
|
|
1858
|
+
if (match === null) continue // malformed predicate — warned + skipped
|
|
1859
|
+
el.hidden = !match
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// The current value of the OWNED field controlling a show binding, as the
|
|
1864
|
+
// string the literal predicate compares against. Mirrors #collectFields'
|
|
1865
|
+
// per-kind reads: a checkbox reports its checked state ("true"/"false" — its
|
|
1866
|
+
// .value is the constant "on", and the checkbox wins over the hidden input
|
|
1867
|
+
// Rails pairs with it); a radio group reports the CHECKED radio's value (""
|
|
1868
|
+
// when none is); anything else reports .value first-wins. Returns null when
|
|
1869
|
+
// no owned field carries the name — the caller then leaves visibility alone.
|
|
1870
|
+
#showFieldValue(name, owns) {
|
|
1871
|
+
let sawRadio = false
|
|
1872
|
+
let first = null
|
|
1873
|
+
for (const el of this.element.querySelectorAll(`[name="${name}"]`)) {
|
|
1874
|
+
if (!owns(el)) continue
|
|
1875
|
+
if (el.type === "checkbox") return el.checked ? "true" : "false"
|
|
1876
|
+
if (el.type === "radio") {
|
|
1877
|
+
if (el.checked) return el.value ?? ""
|
|
1878
|
+
sawRadio = true
|
|
1879
|
+
continue
|
|
1880
|
+
}
|
|
1881
|
+
first ??= el
|
|
1882
|
+
}
|
|
1883
|
+
if (first) return first.value ?? ""
|
|
1884
|
+
return sawRadio ? "" : null
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Remove the show-sync listeners on disconnect, so a stray event after a
|
|
1888
|
+
// Turbo morph/navigation never re-evaluates against a detached root.
|
|
1889
|
+
#teardownShowSync() {
|
|
1890
|
+
if (!this.#boundSyncShow) return
|
|
1891
|
+
this.element.removeEventListener?.("input", this.#boundSyncShow)
|
|
1892
|
+
this.element.removeEventListener?.("change", this.#boundSyncShow)
|
|
1893
|
+
this.element.removeEventListener?.("turbo:morph-element", this.#boundSyncShow)
|
|
1894
|
+
this.#boundSyncShow = undefined
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1776
1897
|
// Build the multipart body (issue #34). `token`/`act` are flat fields the
|
|
1777
1898
|
// endpoint reads from params[:token]/params[:act]; scalar params nest under
|
|
1778
1899
|
// params[<key>] (Rails parses the bracket into params[:params]); each file is
|
|
@@ -1,4 +1,4 @@
|
|
|
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};
|
|
1
|
+
import{Controller as S}from"@hotwired/stimulus";import{confirmResolver as T}from"phlex/reactive/confirm";import{computeReducer as k}from"phlex/reactive/compute";function b(){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 w(){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=B(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)=>t(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(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 Qj(){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 Wj(){x=!1}var P="phlex-reactive:latency",M=!1;function u(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(P,String(j))}function p(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(P),M=!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 Xj(){M=!1}function F(){b(),w(),y(),h(),f(),m()}function c(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)F();else document.addEventListener("turbo:load",F,{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 Zj(){N=!1}function $j(){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 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(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)=>i(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}function r(j,z){let Q=j.getAttribute("data-reactive-show-equals");if(Q!==null)return z===Q;let W=j.getAttribute("data-reactive-show-not");if(W!==null)return z!==W;let X=j.getAttribute("data-reactive-show-in");if(X!==null){try{let Z=JSON.parse(X);if(Array.isArray(Z))return Z.includes(z)}catch{}return console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify(X)} — skipped`),null}return console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped"),null}var a='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function i(j){return j.querySelectorAll?.(a)?.[0]??null}function B(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 t(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 Gj extends S{static values={token:String};#E;#H=new Map;#G=new Map;#f;#M;#F;#N=0;#J=new Map;#L=new WeakMap;#V=new Map;#K;#q;#Y;#j;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.#u()){if(this.#K=()=>this.#O(),this.element.addEventListener?.("turbo:morph-element",this.#K),this.#O(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#Zj()}if(this.#Gj())this.#j=()=>this.#y(),this.element.addEventListener?.("input",this.#j),this.element.addEventListener?.("change",this.#j),this.element.addEventListener?.("turbo:morph-element",this.#j),this.#y()}#u(){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.#z(z))return!0;return!1}disconnect(){this.#s(),this.#n(),this.#$j(),this.#Kj()}dispatch(j){let{action:z,params:Q,debounce:W,throttle:X,confirm:Z,outside:$,window:G,optimistic:q,loading:Y}=j.params;if(!z)return;if($&&this.element.contains(j.target))return;let H=j.currentTarget??j.target;if(!G&&!this.#Uj(q,H))j.preventDefault();if(!Z)return this.#S(H,z,Q,W,X,q,Y);Promise.resolve().then(()=>T(Z)).catch(()=>!1).then((J)=>{if(J)this.#S(H,z,Q,W,X,q,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.#Vj(this.#Hj(z))}trackDirty(){this.#O()}recompute(j){let z=this.#m(),Q=z.map(([J])=>J),W=this.#I(),X=new Map,Z=(J)=>{if(X.has(J))return X.get(J);let K=null;for(let U of this.element.querySelectorAll(`[name="${J}"]`))if(W(U)){K=U;break}return X.set(J,K),K};for(let J of Q)this.#B(J,Z(J)?.value??"");let $=this.element.getAttribute("data-reactive-compute-reducer-param"),G=$?k($):null;if(!G){this.#R({},Z);return}let q=this.#p("data-reactive-compute-outputs-param"),Y={};for(let[J,K]of z){let U=Z(J);if(K==="string")Y[J]=U?.value??"";else{let _=Number(U?.value);Y[J]=Number.isFinite(_)?_:0}}let H=G(Y,{changed:this.#c(j,Q)})||{};for(let J of q){if(!(J in H))continue;let K=Z(J);if(K){if(String(H[J])===K.value)continue;K.value=H[J],K.dispatchEvent(new Event("input",{bubbles:!0}))}else this.#B(J,H[J])}this.#R(H,Z)}listnavNext(j){this.#C(j,1)}listnavPrev(j){this.#C(j,-1)}listnavPick(j){let z=this.#A(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.#A(j))z.removeAttribute("data-reactive-highlighted")}#C(j,z){let Q=this.#A(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"})}#A(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.#I();return Array.from(this.element.querySelectorAll(Q)).filter(W)}#p(j){let z=this.element.getAttribute(j);if(!z)return[];try{let Q=JSON.parse(z);return Array.isArray(Q)?Q:[]}catch{return[]}}#m(){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[]}}#c(j,z){let Q=j?.target;if(!Q?.name||typeof Q.closest!=="function")return null;if(!z.includes(Q.name))return null;return this.#z(Q)?Q.name:null}#B(j,z){let Q=String(z);for(let W of this.#g(j)){if(W.textContent===Q)continue;W.textContent=Q}}#g(j){let z=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(z).filter((Q)=>this.#z(Q))}#R(j,z){let Q=this.#d();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 q of document.querySelectorAll(G)){if(q.textContent===$)continue;q.textContent=$}}}}#d(){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{}}}#S(j,z,Q,W,X,Z,$){if(this.#_("reactive:before-dispatch",{action:z,params:this.#h(Q),element:this.element},{cancelable:!0}).defaultPrevented)return;let q=Number(W)||0;if(q>0)return this.#l(j,q,z,Q,Z,$);let Y=Number(X)||0;if(Y>0)return this.#o(j,Y,z,Q,Z,$);return this.#U(z,Q,Z,j,$)}#U(j,z,Q,W,X){let Z=this.#_j(Q,W),$=this.#Mj(j,W,X);return this.queue=(this.queue??Promise.resolve()).then(()=>this.#zj(j,z,Z,$)),this.queue}#l(j,z,Q,W,X,Z){this.#D(j);let $=()=>{this.#D(j),this.#U(Q,W,X,j,Z)},G=setTimeout($,z);j?.addEventListener?.("blur",$,{once:!0}),this.#H.set(j,{timer:G,flush:$})}#D(j){let z=this.#H.get(j);if(!z)return;clearTimeout(z.timer),j?.removeEventListener?.("blur",z.flush),this.#H.delete(j)}#s(){for(let j of[...this.#H.keys()])this.#D(j)}#o(j,z,Q,W,X,Z){let $=this.#G.get(j)??new Map;if($.has(Q))return;let G=setTimeout(()=>{if($.delete(Q),$.size===0)this.#G.delete(j)},z);return $.set(Q,G),this.#G.set(j,$),this.#U(Q,W,X,j,Z)}#n(){for(let j of this.#G.values())for(let z of j.values())clearTimeout(z);this.#G.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}#Q(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.#U(j,z)};this.#_("reactive:error",{action:j,params:Q,...W,retry:X})}#W(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#r(){this.element?.removeAttribute?.("data-reactive-error")}#a(){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))}#i(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(P));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!M)M=!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))}#t(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#T(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#e(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}#jj(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#zj(j,z,Q,W){let{fields:X,files:Z}=this.#Wj(),$=this.#h(z),G={...X,...$},q=this.#X,Y=Z.length>0,H=Y?this.#qj(q,j,G,Z):JSON.stringify({token:q,act:j,params:G}),J=this.#t()?{action:j,paramNames:Object.keys($),fieldNames:Object.keys(X),encoding:Y?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#T()}:null;await this.#i();try{if(navigator.onLine===!1){this.#Z(Q),this.#W("offline"),this.#Q(j,z,G,{kind:"offline"});return}let K;try{let V={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#Ej()};if(!Y)V["Content-Type"]="application/json";let I=this.#Fj();if(I)V["X-Pgbus-Connection"]=I;K=await fetch(this.#xj(),{method:"POST",headers:V,body:H,credentials:"same-origin",signal:AbortSignal.timeout(this.#Pj())})}catch(V){if(console.error("[phlex-reactive] action error",V),this.#Z(Q),V?.name==="TimeoutError"||V?.name==="AbortError"){this.#W("timeout"),this.#Q(j,z,G,{kind:"timeout"});return}this.#a(),this.#W("network"),this.#Q(j,z,G,{kind:"network"});return}if(J)J.status=K.status;if(K.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#Z(Q),this.#W("redirected"),this.#Q(j,z,G,{kind:"redirected",status:K.status});return}if(!K.ok){let V=await K.text();if(console.error(`[phlex-reactive] action failed: HTTP ${K.status}`,V),this.#Z(Q),(K.headers.get("Content-Type")||"").includes("turbo-stream")){let I=this.#b(V);if(this.#X=I??this.#X,J)this.#k(J,V,I);window.Turbo.renderStreamMessage(V)}this.#W("http"),this.#Q(j,z,G,{kind:"http",status:K.status,body:V});return}let U=K.headers.get("Content-Type")||"";if(!U.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${U}" — no update applied`),this.#Z(Q),this.#W("content-type"),this.#Q(j,z,G,{kind:"content-type",status:K.status});return}let _=await K.text(),E=this.#b(_);if(this.#X=E??this.#X,J)this.#k(J,_,E);window.Turbo.renderStreamMessage(_),this.#r(),this.#_("reactive:applied",{action:j,params:G,html:_})}catch(K){console.error("[phlex-reactive] action error",K),this.#Z(Q),this.#_("reactive:error",{action:j,params:G,kind:"apply"})}finally{if(W?.(),J)this.#jj({...J,ms:this.#T()-J.started})}}#k(j,z,Q){j.streams=this.#e(z),j.tokenRefreshed=Q!=null}get#X(){return this.#E??this.tokenValue}set#X(j){this.#E=j}#b(j){let z=this.element.id;if(!z)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:Q,self:W}=this.#Qj(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}#Qj(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>`)}}#z(j){return j.closest('[data-controller~="reactive"]')===this.element}#I(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(z)=>this.#z(z)}#Wj(){let j={},z=[],Q=this.#I();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}}#O(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((z)=>{if(!this.#z(z))return;if(z.type==="file")return;if(this.#Xj(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")}#Xj(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.#Y=(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.#Y)}#$j(){if(this.#K)this.element.removeEventListener?.("turbo:morph-element",this.#K),this.#K=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#q)window.removeEventListener("beforeunload",this.#q);if(this.#Y)window.removeEventListener("turbo:before-visit",this.#Y)}this.#q=void 0,this.#Y=void 0}#Gj(){let j=this.element.querySelectorAll?.("[data-reactive-show-field]")??[];for(let z of j)if(this.#z(z))return!0;return!1}#y(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.#I(),z=new Map;for(let Q of this.element.querySelectorAll("[data-reactive-show-field]")){if(!j(Q))continue;let W=Q.getAttribute("data-reactive-show-field");if(!W)continue;if(!z.has(W))z.set(W,this.#Jj(W,j));let X=z.get(W);if(X===null)continue;let Z=r(Q,X);if(Z===null)continue;Q.hidden=!Z}}#Jj(j,z){let Q=!1,W=null;for(let X of this.element.querySelectorAll(`[name="${j}"]`)){if(!z(X))continue;if(X.type==="checkbox")return X.checked?"true":"false";if(X.type==="radio"){if(X.checked)return X.value??"";Q=!0;continue}W??=X}if(W)return W.value??"";return Q?"":null}#Kj(){if(!this.#j)return;this.element.removeEventListener?.("input",this.#j),this.element.removeEventListener?.("change",this.#j),this.element.removeEventListener?.("turbo:morph-element",this.#j),this.#j=void 0}#qj(j,z,Q,W){let X=new FormData;X.append("token",j),X.append("act",z);for(let[$,G]of Object.entries(Q))this.#x(X,`params[${$}]`,G);let Z=this.#Yj(W);for(let{name:$,file:G,multiple:q}of W){let H=q||Z.has($)?`params[${$}][]`:`params[${$}]`;X.append(H,G,G.name)}return X}#x(j,z,Q){if(Q==null)j.append(z,"");else if(Array.isArray(Q))Q.forEach((W,X)=>this.#x(j,`${z}[${X}]`,W));else if(typeof Q==="object")for(let[W,X]of Object.entries(Q))this.#x(j,`${z}[${W}]`,X);else j.append(z,String(Q))}#Yj(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))}#h(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#Hj(j){return B(j)}#Vj(j){R(j,(z)=>this.#P(z))}#P(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.#z(Q))}#Uj(j,z){if(j?.checked!=="keep")return!1;let Q=z?.type;return Q==="checkbox"||Q==="radio"}#_j(j,z){if(!j)return null;let Q=this.#Ij(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}#Z(j){if(!j)return;if(!this.element.isConnected)return;for(let z of j)z()}#Ij(j,z){if(j.to==null)return z?[z]:[];return this.#P({to:j.to})}#Mj(j,z,Q){this.#Nj(j,z);let W=this.#Aj(j,z,Q),X=!1;return()=>{if(X)return;X=!0,this.#Lj(j,z),W()}}#Nj(j,z){if(this.#$(z,j,1),this.#$(this.element,j,1),this.#J.set(j,(this.#J.get(j)??0)+1),this.#N++===0)this.element.setAttribute("aria-busy","true");for(let Q of this.#v(j))this.#$(Q,j,1)}#Lj(j,z){this.#$(z,j,-1),this.#$(this.element,j,-1);let Q=(this.#J.get(j)??1)-1;if(Q<=0)this.#J.delete(j);else this.#J.set(j,Q);if(--this.#N<=0)this.#N=0,this.element.removeAttribute("aria-busy");for(let W of this.#v(j))this.#$(W,j,-1)}#$(j,z,Q){if(!j||typeof j.getAttribute!=="function")return;let W=this.#L.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.#L.delete(j),j.removeAttribute("data-reactive-busy");return}this.#L.set(j,W),j.setAttribute("data-reactive-busy",[...W.keys()].join(" "))}#v(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((Q)=>Q.getAttribute("data-reactive-busy-on")===j&&this.#z(Q))}#Aj(j,z,Q){if(!Q||!z)return()=>{};let W=this.#Oj(Q,z),X=Array.isArray(Q.class)?Q.class:[],Z=[];for(let G of W){let q=X.filter((Y)=>!G.classList.contains(Y));if(G.classList.add(...q),q.length)Z.push([G,q])}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,q]of Z)if(G.isConnected)G.classList.remove(...q);this.#Dj(z,Q)}}#Dj(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}#Oj(j,z){if(j.to==null)return z?[z]:[];return this.#P({to:j.to})}#xj(){return this.#f??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#Pj(){if(this.#M!=null)return this.#M;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,z=Number(j);return this.#M=Number.isFinite(z)&&z>0?z:30000}#Ej(){return document.querySelector('meta[name="csrf-token"]')?.content??""}#Fj(){return document.querySelector("pgbus-stream-source[connection-id]")?.getAttribute("connection-id")||document.querySelector('meta[name="pgbus-connection-id"]')?.content||null}}export{b as registerReactiveVisit,w as registerReactiveToken,f as registerReactiveOffline,y as registerReactiveJs,h as registerReactiveDismiss,F as registerReactiveActions,c as escapeRegExp,u as enableLatencySim,p as disableLatencySim,Gj as default,g as checkReactiveRegistration,Zj as __resetReactiveRegistrationForTest,Wj as __resetReactiveOfflineForTest,Xj as __resetReactiveLatencyForTest,Qj as __resetReactiveDismissForTest,$j as __markReactiveConnectedForTest,P as LATENCY_KEY};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=B0542866EC05173F64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=reactive_controller.min.js.map
|