phlex-reactive 0.9.4 → 0.9.5

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: 2d95213b7c5a3711a00f1992f0a1992363c203aa3bd253875f0ceb67db88986f
4
- data.tar.gz: 93574b53d1e24f5c61d4dca65b65521a32782d5945d99c911484c76ec10ac8ce
3
+ metadata.gz: 9e6f3431087aff47f32d503174b5242c51af5fce1bbdd7832d40a466a5c3a548
4
+ data.tar.gz: 7bed02791a8b41163bd7b237aa1bb2730ff99496450783eae712bbcb1ae46052
5
5
  SHA512:
6
- metadata.gz: 037cfc4cb78a82f4d98b197b59f1dfe1d9ed7bb9222751207bf0b583d1e8ba89bdb30d1cb79ad5543ad5b7e4e5a84a08cc59e0b463bef0e5e5f36f1c31a60850
7
- data.tar.gz: d0d5eb3a48674bb0dc93417b3e61911e43c976457b3203a1e45bf59f0e0e5c33ceebc50e458240b8f8f15158ad81bdb98fb53addc47a36865c4b94e490e7fd67
6
+ metadata.gz: f66d90ab48f9210f0bfddf08f14b2a86593118af903574432592b408920656f3d025cd7a8a067b27faf2c42b8456e8e802caf93980cfa967b3fd69eed0c80646
7
+ data.tar.gz: 6e449424886a2375fc5b64788077086eb7274536eb72d843c9dc5ee0d107e688f573f1a723a283d806dac2897bf55b9e9f432f472bb157360c7d1931aff6aab3
data/CHANGELOG.md CHANGED
@@ -31,6 +31,21 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
31
31
 
32
32
  ### Added
33
33
 
34
+ - **Compound & numeric `reactive_show` predicates — `all:`/`any:` and
35
+ `gte:`/`gt:`/`lte:`/`lt:` (#176).** Value-conditional visibility now spans
36
+ **more than one field** and **numeric thresholds**, staying inside the
37
+ eval-free "declared literal predicate" contract. `all:` / `any:` fold a list of
38
+ per-field terms (`{ field:, equals:/not:/in:/gte:/… }`) with one fixed
39
+ connective — AND vs OR over the same literal vocabulary, no expression surface;
40
+ one flat binding replaces wrapper-div nesting and is the only way to express OR.
41
+ `gte:`/`gt:`/`lte:`/`lt:` compare `Number(value)` against a literal number baked
42
+ into the binding (the RHS must be a real `Numeric` — a typo fails at render); a
43
+ non-numeric field value is `NaN` → hidden, the safe reveal-on-threshold default.
44
+ Numeric predicates work standalone, as a compound term, and inside a
45
+ `reactive_show_targets` map. Malformed terms fold **false** (fail-closed:
46
+ default-deny). The single-field `reactive_show(:field, equals:)` form is
47
+ unchanged; the additions are backwards compatible.
48
+
34
49
  - **Installable Claude debugging skill + `rails g phlex:reactive:claude` (#168).**
35
50
  The gem ships a `phlex-reactive-debugging` skill (the doctor → inventory → find
36
51
  → browser `report()` → MCP workflow + a failure table) under `lib/`, and the
@@ -766,10 +766,59 @@ function showBindingMatches(el, value) {
766
766
  console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify(inRaw)} — skipped`)
767
767
  return null
768
768
  }
769
+ // Numeric threshold predicates (issue #176 part B): gte/gt/lte/lt read the
770
+ // literal off its own flat attr and compare Number(value) against it. Any
771
+ // present numeric attr decides the binding — a non-numeric field value (NaN)
772
+ // is false (hidden), and a non-numeric LITERAL warn-skips (null).
773
+ for (const key of SHOW_NUMERIC_KEYS) {
774
+ const raw = el.getAttribute(`data-reactive-show-${key}`)
775
+ if (raw !== null) return numericPredicateMatches(key, raw, value)
776
+ }
769
777
  console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped")
770
778
  return null
771
779
  }
772
780
 
781
+ // The numeric threshold keys (issue #176 part B) — the client half of the Ruby
782
+ // SHOW_NUMERIC_KEYS. Order-independent; the evaluator reads the one that's
783
+ // present. Each coerces BOTH sides to Number and compares.
784
+ const SHOW_NUMERIC_KEYS = ["gte", "gt", "lte", "lt"]
785
+
786
+ // Evaluate one numeric threshold predicate against a field value. Returns
787
+ // true/false for a decidable comparison, or null when the LITERAL itself is
788
+ // non-numeric (a malformed binding — warn-skip, default-deny). A non-numeric
789
+ // FIELD value (empty/blank/garbage) is treated as NaN → false: the
790
+ // reveal-on-threshold notice stays hidden, the safe default. Shared by the
791
+ // owned-binding evaluator (raw string literal off an attr) and the
792
+ // cross-root/compound evaluator (a literal that arrived as a JSON number or
793
+ // string).
794
+ function numericPredicateMatches(key, literal, value) {
795
+ const rhs = Number(literal)
796
+ if (Number.isNaN(rhs)) {
797
+ console.warn(`[phlex-reactive] reactive_show ${key}: needs a numeric literal, got ${JSON.stringify(literal)} — skipped`)
798
+ return null
799
+ }
800
+ // A blank/whitespace field value must fail closed. Number("") and
801
+ // Number(" ") are 0 (NOT NaN), so a bare Number()+isNaN check would wrongly
802
+ // reveal a `lte:`/`lt:`/`gte: 0` binding on an EMPTY field. Force the
803
+ // empty/blank case to NaN so the "blank → hidden" contract holds for every
804
+ // operator, not just the ones where 0 happens to fail the comparison.
805
+ const trimmed = value == null ? "" : String(value).trim()
806
+ const n = trimmed === "" ? NaN : Number(trimmed)
807
+ if (Number.isNaN(n)) return false
808
+ switch (key) {
809
+ case "gte":
810
+ return n >= rhs
811
+ case "gt":
812
+ return n > rhs
813
+ case "lte":
814
+ return n <= rhs
815
+ case "lt":
816
+ return n < rhs
817
+ default:
818
+ return null
819
+ }
820
+ }
821
+
773
822
  // Evaluate an ALREADY-PARSED show predicate object (issue #164) — the
774
823
  // reactive_show_targets map embeds { equals/not/in } directly in its JSON, so
775
824
  // unlike showBindingMatches there are no attrs to read or re-parse. The same
@@ -781,9 +830,63 @@ function showPredicateMatches(pred, value) {
781
830
  if (typeof pred.equals === "string") return value === pred.equals
782
831
  if (typeof pred.not === "string") return value !== pred.not
783
832
  if (Array.isArray(pred.in)) return pred.in.includes(value)
833
+ // Numeric threshold predicates (issue #176 part B): the literal arrives as a
834
+ // JSON number (or a numeric string) embedded in the predicate object — one
835
+ // shared numericPredicateMatches with the owned-binding evaluator.
836
+ for (const key of SHOW_NUMERIC_KEYS) {
837
+ if (key in pred) return numericPredicateMatches(key, pred[key], value)
838
+ }
839
+ return null
840
+ }
841
+
842
+ // The selector matching every OWNED-element show binding: single-field
843
+ // (data-reactive-show-field, issue #161) OR compound all:/any:
844
+ // (data-reactive-show, issue #176). Both the connect() gate and the sync walk
845
+ // use it so a compound-only root still enables the sync.
846
+ const SHOW_BINDING_SELECTOR = "[data-reactive-show-field], [data-reactive-show]"
847
+
848
+ // Parse a compound show binding's JSON payload (issue #176 part A). Malformed
849
+ // JSON degrades to null WITH a warn — a bad binding must never throw or blank
850
+ // the page (client-side default-deny), but a collision (two bindings' JSON
851
+ // mix-joined) is worth surfacing.
852
+ function parseShowCompound(raw) {
853
+ try {
854
+ const parsed = JSON.parse(raw)
855
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed
856
+ } catch {
857
+ // fall through to the warn
858
+ }
859
+ console.warn(`[phlex-reactive] malformed compound reactive_show payload ${JSON.stringify(raw)} — skipped`)
784
860
  return null
785
861
  }
786
862
 
863
+ // Evaluate a COMPOUND show binding (issue #176 part A): a parsed
864
+ // { all: [term, …] } or { any: [term, …] } payload, where each term is
865
+ // { field, <predicate> }. `fieldValue(name)` resolves the OWNED field's current
866
+ // value (memoized by the caller). all: ANDs every term, any: ORs them. A term
867
+ // whose field is missing (null) or whose predicate is malformed/unknown folds
868
+ // as FALSE — fail-closed (the issue's default-deny lean): a broken AND term can
869
+ // never pass, a broken OR term can never reveal. Returns true/false for a
870
+ // decidable payload, or null for a malformed payload (no all:/any: array) so
871
+ // the caller warn-skips and leaves visibility alone.
872
+ function compoundShowMatches(payload, fieldValue) {
873
+ if (!payload || typeof payload !== "object") return null
874
+ const connective = Array.isArray(payload.all) ? "all" : Array.isArray(payload.any) ? "any" : null
875
+ if (!connective) return null
876
+ const terms = payload[connective]
877
+ if (terms.length === 0) return null // an empty compound decides nothing — skip
878
+
879
+ const results = terms.map((term) => {
880
+ if (!term || typeof term !== "object" || typeof term.field !== "string") return false
881
+ const value = fieldValue(term.field)
882
+ if (value === null) return false // no owned field with that name → fail-closed
883
+ const match = showPredicateMatches(term, value)
884
+ return match === true // null (malformed term) or false both fold to false
885
+ })
886
+
887
+ return connective === "all" ? results.every(Boolean) : results.some(Boolean)
888
+ }
889
+
787
890
  // A cross-root show target must be a single ID selector (issue #164) — the
788
891
  // SAME shape the #159 mirror enforces (one shared regex), with its own warn so
789
892
  // a refused show target is distinguishable in the console. The client half of
@@ -2222,7 +2325,10 @@ export default class extends Controller {
2222
2325
  // getAttribute, cheaper than the binding walk.
2223
2326
  #showSyncEnabled() {
2224
2327
  if (this.element.getAttribute?.("data-reactive-show-targets")) return true
2225
- const nodes = this.element.querySelectorAll?.("[data-reactive-show-field]") ?? []
2328
+ // Single-field bindings carry -field; compound all:/any: bindings (issue
2329
+ // #176) carry data-reactive-show and have NO single controlling field, so
2330
+ // both selectors gate the sync.
2331
+ const nodes = this.element.querySelectorAll?.(SHOW_BINDING_SELECTOR) ?? []
2226
2332
  for (const el of nodes) if (this.#ownsField(el)) return true
2227
2333
  return false
2228
2334
  }
@@ -2240,12 +2346,27 @@ export default class extends Controller {
2240
2346
 
2241
2347
  const owns = this.#ownershipFilter()
2242
2348
  const values = new Map()
2243
- for (const el of this.element.querySelectorAll("[data-reactive-show-field]")) {
2349
+ // A memoized resolver shared by every binding in this pass — a field driving
2350
+ // several bindings (and several terms of a compound) reads exactly once.
2351
+ const fieldValue = (name) => {
2352
+ if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns))
2353
+ return values.get(name)
2354
+ }
2355
+ for (const el of this.element.querySelectorAll(SHOW_BINDING_SELECTOR)) {
2244
2356
  if (!owns(el)) continue // a nested root's binding is its own controller's job
2357
+
2358
+ // Compound all:/any: (issue #176 part A): no single -field attr; parse the
2359
+ // JSON payload and fold its per-field terms.
2360
+ const compoundRaw = el.getAttribute("data-reactive-show")
2361
+ if (compoundRaw !== null) {
2362
+ const match = compoundShowMatches(parseShowCompound(compoundRaw), fieldValue)
2363
+ if (match !== null) el.hidden = !match
2364
+ continue
2365
+ }
2366
+
2245
2367
  const name = el.getAttribute("data-reactive-show-field")
2246
2368
  if (!name) continue
2247
- if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns))
2248
- const value = values.get(name)
2369
+ const value = fieldValue(name)
2249
2370
  if (value === null) continue // no owned field with that name — leave it be
2250
2371
  const match = showBindingMatches(el, value)
2251
2372
  if (match === null) continue // malformed predicate — warned + skipped
@@ -1,4 +1,4 @@
1
- import{Controller as p}from"@hotwired/stimulus";import{confirmResolver as m}from"phlex/reactive/confirm";import{computeReducer as c}from"phlex/reactive/compute";function g(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:visit"])return;j["reactive:visit"]=function(){let Q=this.getAttribute("data-url");if(Q)window.Turbo.visit(Q,{action:"advance"})}}function d(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:token"])return;j["reactive:token"]=function(){let Q=this.getAttribute("data-reactive-token-value"),W=this.getAttribute("target");if(!Q||!W)return;let X=document.getElementById(W);if(X)X.setAttribute("data-reactive-token-value",Q)}}function l(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:js"])return;j["reactive:js"]=function(){let Q=f(this.getAttribute("data-reactive-ops"));if(!Q.length)return;let W=this.getAttribute("target"),X=W?document.getElementById(W):null;if(W&&!X)return;u(Q,(Z)=>Mj(Z,X))}}var U=new Map;function Oj(){U.clear(),P=!1}function xj(j){return U.get(j)?.via}var P=!1;function s(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:defer"])return;if(j["reactive:defer"]=function(){let Q=this.getAttribute("target");if(!Q)return;if(this.getAttribute("data-reactive-defer-via")==="stream"){n(Q,this);return}let W=this.getAttribute("data-reactive-defer-token");if(!W)return;L(Q,W)},!P&&typeof document<"u"&&document.addEventListener)P=!0,document.addEventListener("turbo:before-stream-render",o)}function o(j){let W=j.target?.getAttribute?.("target");if(!W)return;let X=W.startsWith("reactive-defer-src-")?W.slice(19):W;if(U.get(X)?.via==="stream")U.delete(X)}function L(j,Q){let W=document.getElementById(j);if(!W){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}b(j),h(W);let X={via:"fetch",abort:new AbortController,timedOut:!1};U.set(j,X),i(j,X,Q)}function n(j,Q){let W=document.getElementById(j);if(!W){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}let X=Q.getAttribute("data-reactive-defer-src");if(!X)return;if(!globalThis.customElements?.get?.("pgbus-stream-source")){let $=Q.getAttribute("data-reactive-defer-token");if($){L(j,$);return}console.error("[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered "+"and no fallback token was provided — is the pgbus client loaded on this page?");return}b(j),h(W);let Z=document.createElement("pgbus-stream-source");Z.id=w(j),Z.setAttribute("src",X),Z.setAttribute("since-id",Q.getAttribute("data-reactive-defer-since-id")??"0"),Z.setAttribute("hidden",""),document.body.appendChild(Z),U.set(j,{via:"stream"})}function w(j){return`reactive-defer-src-${j}`}async function i(j,Q,W){let X=setTimeout(()=>{Q.timedOut=!0,Q.abort.abort()},t()),Z;try{Z=await fetch(a(),{method:"POST",headers:{Accept:"text/vnd.turbo-stream.html","Content-Type":"application/json","X-CSRF-Token":r()},body:JSON.stringify({token:W}),credentials:"same-origin",signal:Q.abort.signal})}catch(G){if(clearTimeout(X),U.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed",G),x(j,W);return}if(U.get(j)!==Q){clearTimeout(X);return}if(Z.status===204){clearTimeout(X),I(j);return}if(!Z.ok){clearTimeout(X),console.error(`[phlex-reactive] deferred render failed: HTTP ${Z.status}`),x(j,W,Z.status);return}let $;try{$=await Z.text()}catch(G){if(clearTimeout(X),U.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed reading the body",G),x(j,W);return}if(clearTimeout(X),U.get(j)!==Q)return;I(j),window.Turbo.renderStreamMessage($)}function b(j){let Q=U.get(j);if(!Q)return;if(U.delete(j),Q.via==="fetch")Q.abort.abort();else document.getElementById(w(j))?.remove?.()}function h(j){j.setAttribute("data-reactive-defer-pending","true"),j.setAttribute("aria-busy","true")}function y(j){j.removeAttribute("data-reactive-defer-pending"),j.removeAttribute("aria-busy")}function I(j){U.delete(j);let Q=document.getElementById(j);if(!Q)return;y(Q),Q.removeAttribute("data-reactive-error")}function x(j,Q,W){U.delete(j);let X=document.getElementById(j);if(!X)return;y(X),X.setAttribute("data-reactive-error","defer");let Z=()=>{let $=document.getElementById(j);if(!$){console.warn("[phlex-reactive] defer retry() ignored — the target left the DOM");return}$.removeAttribute("data-reactive-error"),L(j,Q)};X.dispatchEvent(new CustomEvent("reactive:error",{bubbles:!0,composed:!0,detail:{kind:"defer",target:j,status:W,retry:Z}}))}function a(){return document.querySelector('meta[name="phlex-reactive-defer-path"]')?.content||"/reactive/defer"}function r(){return document.querySelector('meta[name="csrf-token"]')?.content??""}function t(){let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:30000}var F=!1;function e(){if(F)return;if(typeof document>"u"||!document.addEventListener)return;F=!0,document.addEventListener("turbo:before-stream-render",jj)}function jj(j){let Q=j.detail,W=Q?.render;if(typeof W!=="function"||W.__reactiveDismissWrapped){if(typeof requestAnimationFrame==="function")requestAnimationFrame(B);else setTimeout(B,0);return}let X=async(Z)=>{await W(Z),B()};X.__reactiveDismissWrapped=!0,Q.render=X}function B(){let j=document.querySelectorAll("[data-reactive-dismiss-after]");for(let Q of j){if(Q.hasAttribute("data-reactive-dismiss-scheduled"))continue;let W=Number(Q.getAttribute("data-reactive-dismiss-after"));if(!Number.isFinite(W)||W<=0)continue;Q.setAttribute("data-reactive-dismiss-scheduled",""),setTimeout(()=>Q.remove(),W)}}function Bj(){F=!1}var R=!1;function Qj(){if(R)return;if(typeof window>"u"||typeof document>"u")return;if(typeof window.addEventListener!=="function")return;R=!0;let j=()=>{let Q=document.documentElement;if(typeof Q?.toggleAttribute!=="function")return;Q.toggleAttribute("data-reactive-offline",globalThis.navigator?.onLine===!1)};j(),window.addEventListener("online",j),window.addEventListener("offline",j)}function Cj(){R=!1}var D="phlex-reactive:latency",A=!1;function Wj(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(D,String(j))}function Xj(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(D),A=!1}function Zj(){if(typeof window>"u"||typeof document>"u")return;if(document.querySelector?.('meta[name="phlex-reactive-env"]')?.content!=="development")return;window.PhlexReactive={enableLatencySim:Wj,disableLatencySim:Xj}}function Ej(){A=!1}function S(){g(),d(),l(),s(),e(),Qj(),Zj()}function $j(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)S();else document.addEventListener("turbo:load",S,{once:!0});var O=!1;function Gj(){if(O)return;if(typeof document>"u")return;let j=document.querySelectorAll('[data-controller~="reactive"]');if(!j||j.length===0)return;console.warn("[phlex-reactive] found "+j.length+' element(s) with data-controller="reactive" '+"but the reactive controller never connected. It is loaded but not registered — "+'add `application.register("reactive", ReactiveController)` (importmap) or import it into app/javascript/controllers/ for lazyLoadControllersFrom apps. See the README.')}function Pj(){O=!1}function Fj(){O=!0}if(typeof window<"u"&&typeof document<"u"){let j=()=>setTimeout(Gj,0);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",j,{once:!0});else j()}var Jj=new Set(["href","src","srcdoc","action","formaction","xlink:href","style"]);function Kj(j){let Q=String(j).toLowerCase();return Q.startsWith("on")||Jj.has(Q)}function zj(j,Q,W){let[X,Z,$]=Q;j.classList.add(X,Z),W(),requestAnimationFrame(()=>{j.classList.remove(Z),j.classList.add($)});let G=!1,J=()=>{if(G)return;G=!0,j.classList.remove(X,$)};j.addEventListener("animationend",J,{once:!0}),setTimeout(J,350)}var k=Object.freeze({show:(j,Q)=>C(j,!1,Q),hide:(j,Q)=>C(j,!0,Q),toggle:(j,Q)=>C(j,!j.hidden,Q),add_class:(j,Q)=>j.classList.add(...Q.classes??[]),remove_class:(j,Q)=>j.classList.remove(...Q.classes??[]),toggle_class:(j,Q)=>(Q.classes??[]).forEach((W)=>j.classList.toggle(W)),set_attr:(j,Q)=>{if(E(Q.name))j.setAttribute(Q.name,Q.value??"")},remove_attr:(j,Q)=>{if(E(Q.name))j.removeAttribute(Q.name)},toggle_attr:(j,Q)=>{if(!E(Q.name))return;if(j.hasAttribute(Q.name))j.removeAttribute(Q.name);else j.setAttribute(Q.name,"")},focus:(j)=>j.focus?.(),focus_first:(j)=>_j(j)?.focus?.(),text:(j,Q)=>{let W=String(Q.value??"");if(j.textContent!==W)j.textContent=W},dispatch:(j,Q)=>{j.dispatchEvent(new CustomEvent(Q.name,{bubbles:!0,composed:!0,detail:Q.detail??{}}))}});function C(j,Q,W){if(W?.transition)zj(j,W.transition,()=>j.hidden=Q);else j.hidden=Q}function E(j){if(!Kj(j))return!0;return console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(j)} — skipped`),!1}var v=/^#[A-Za-z_][\w-]*$/;function Yj(j){if(typeof j==="string"&&v.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(j)} — skipped`),!1}function qj(j,Q){let W=j.getAttribute("data-reactive-show-equals");if(W!==null)return Q===W;let X=j.getAttribute("data-reactive-show-not");if(X!==null)return Q!==X;let Z=j.getAttribute("data-reactive-show-in");if(Z!==null){try{let $=JSON.parse(Z);if(Array.isArray($))return $.includes(Q)}catch{}return console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify(Z)} — skipped`),null}return console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped"),null}function Hj(j,Q){if(!j||typeof j!=="object")return null;if(typeof j.equals==="string")return Q===j.equals;if(typeof j.not==="string")return Q!==j.not;if(Array.isArray(j.in))return j.in.includes(Q);return null}function Uj(j){if(typeof j==="string"&&v.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root show target ${JSON.stringify(j)} — skipped`),!1}var Vj='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function _j(j){return j.querySelectorAll?.(Vj)?.[0]??null}function f(j){if(Array.isArray(j))return j;if(typeof j!=="string")return[];try{let Q=JSON.parse(j);return Array.isArray(Q)?Q:[]}catch{return[]}}function u(j,Q){for(let W of j){if(!Array.isArray(W))continue;let[X,Z={}]=W;if(!Object.hasOwn(k,X)){console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(X)} — skipped`);continue}for(let $ of Q(Z))k[X]($,Z)}}function Mj(j,Q){let W=j.to;if(Q){if(W==="@root")return[Q];if(typeof W!=="string"||W==="")return[];if(j.global)return[...document.querySelectorAll(W)];return[...Q.querySelectorAll(W)]}if(typeof W!=="string"||W===""||W==="@root")return[];return[...document.querySelectorAll(W)]}class Rj extends p{static values={token:String};#R;#V=new Map;#K=new Map;#g;#L;#D;#O=0;#z=new Map;#x=new WeakMap;#_=new Map;#Y;#q;#H;#j;#W;#M;connect(){if(O=!0,this.element.id==="")console.warn("[phlex-reactive] a reactive root has no id; its next-action token can't self-match "+"and may fall back to the first token in the response → a silent HTTP 403 on the NEXT action. "+"Put id: on the SAME element as reactive_attrs — use div(**reactive_root) (emits id + token together), "+"or div(id:, **reactive_attrs). The id: must NOT be on a child. See the README.");if(this.element.getAttribute?.("data-reactive-defer-token"))this.#T(),this.#M=()=>this.#T(),this.element.addEventListener?.("turbo:morph-element",this.#M);if(this.#d()){if(this.#Y=()=>this.#E(),this.element.addEventListener?.("turbo:morph-element",this.#Y),this.#E(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#Yj()}if(this.#Hj())this.#j=()=>this.#f(),this.element.addEventListener?.("input",this.#j),this.element.addEventListener?.("change",this.#j),this.element.addEventListener?.("turbo:morph-element",this.#j),this.#f();if(this.#Mj())this.#W=(j)=>{if(j?.type==="input"&&!this.#Nj(j))return;this.#p()},this.element.addEventListener?.("input",this.#W),this.element.addEventListener?.("turbo:morph-element",this.#W),this.#p()}#d(){if((this.element.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let j=this.element.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}disconnect(){if(this.#r(),this.#e(),this.#qj(),this.#_j(),this.#Aj(),this.#M)this.element.removeEventListener?.("turbo:morph-element",this.#M)}#T(){let j=this.element;if(!j?.id)return;let Q=j.getAttribute?.("data-reactive-defer-token");if(!Q)return;if(j.getAttribute?.("data-reactive-defer-pending")!=="true")return;L(j.id,Q)}dispatch(j){let{action:Q,params:W,debounce:X,throttle:Z,confirm:$,outside:G,window:J,optimistic:K,loading:q}=j.params;if(!Q)return;if(G&&this.element.contains(j.target))return;let H=j.currentTarget??j.target;if(!J&&!this.#Cj(K,H))j.preventDefault();if(!$)return this.#w(H,Q,W,X,Z,K,q);Promise.resolve().then(()=>m($)).catch(()=>!1).then((z)=>{if(z)this.#w(H,Q,W,X,Z,K,q)})}runOps(j){let{ops:Q,outside:W,window:X}=j.params;if(W&&this.element.contains(j.target))return;if(!X)j.preventDefault();this.#Bj(this.#xj(Q))}trackDirty(){this.#E()}recompute(j){let Q=this.#s(),W=Q.map(([z])=>z),X=this.#U(),Z=new Map,$=(z)=>{if(Z.has(z))return Z.get(z);let Y=null;for(let _ of this.element.querySelectorAll(`[name="${z}"]`))if(X(_)){Y=_;break}return Z.set(z,Y),Y};for(let z of W)this.#S(z,$(z)?.value??"");let G=this.element.getAttribute("data-reactive-compute-reducer-param"),J=G?c(G):null;if(!J){this.#k({},$);return}let K=this.#l("data-reactive-compute-outputs-param"),q={};for(let[z,Y]of Q){let _=$(z);if(Y==="string")q[z]=_?.value??"";else{let M=Number(_?.value);q[z]=Number.isFinite(M)?M:0}}let H=J(q,{changed:this.#o(j,W)})||{};for(let z of K){if(!(z in H))continue;let Y=$(z);if(Y){if(String(H[z])===Y.value)continue;Y.value=H[z],Y.dispatchEvent(new Event("input",{bubbles:!0}))}else this.#S(z,H[z])}this.#k(H,$)}listnavNext(j){this.#I(j,1)}listnavPrev(j){this.#I(j,-1)}listnavPick(j){let Q=this.#B(j),W=Q.findIndex((X)=>X.hasAttribute("data-reactive-highlighted"));if(W<0)return;j.preventDefault(),Q[W].click()}listnavClose(j){for(let Q of this.#B(j))Q.removeAttribute("data-reactive-highlighted")}#I(j,Q){let W=this.#B(j);if(!W.length)return;j.preventDefault();let X=W.findIndex((G)=>G.hasAttribute("data-reactive-highlighted")),Z=X<0?Q>0?0:W.length-1:(X+Q+W.length)%W.length;for(let G of W)G.removeAttribute("data-reactive-highlighted");let $=W[Z];$.setAttribute("data-reactive-highlighted","true"),$.scrollIntoView?.({block:"nearest"})}#B(j){let W=(j?.currentTarget??j?.target??this.element).getAttribute?.("data-reactive-listnav-option-param")??this.element.getAttribute("data-reactive-listnav-option-param");if(!W)return[];let X=this.#U();return Array.from(this.element.querySelectorAll(W)).filter((Z)=>!Z.hidden&&X(Z))}#l(j){let Q=this.element.getAttribute(j);if(!Q)return[];try{let W=JSON.parse(Q);return Array.isArray(W)?W:[]}catch{return[]}}#s(){let j=this.element.getAttribute("data-reactive-compute-inputs-param");if(!j)return[];try{let Q=JSON.parse(j);if(Array.isArray(Q))return Q.map((W)=>[W,"number"]);if(Q&&typeof Q==="object")return Object.entries(Q);return[]}catch{return[]}}#o(j,Q){let W=j?.target;if(!W?.name||typeof W.closest!=="function")return null;if(!Q.includes(W.name))return null;return this.#Q(W)?W.name:null}#S(j,Q){let W=String(Q);for(let X of this.#n(j)){if(X.textContent===W)continue;X.textContent=W}}#n(j){let Q=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(Q).filter((W)=>this.#Q(W))}#k(j,Q){let W=this.#i();for(let[X,Z]of Object.entries(W)){let $=X in j?j[X]:Q(X)?.value;if($===void 0||$===null)continue;let G=String($);for(let J of Array.isArray(Z)?Z:[Z]){if(!Yj(J))continue;for(let K of document.querySelectorAll(J)){if(K.textContent===G)continue;K.textContent=G}}}}#i(){let j=this.element.getAttribute("data-reactive-compute-mirror-param");if(!j)return{};try{let Q=JSON.parse(j);return Q&&typeof Q==="object"&&!Array.isArray(Q)?Q:{}}catch{return{}}}#w(j,Q,W,X,Z,$,G){if(this.#A("reactive:before-dispatch",{action:Q,params:this.#m(W),element:this.element},{cancelable:!0}).defaultPrevented)return;let K=Number(X)||0;if(K>0)return this.#a(j,K,Q,W,$,G);let q=Number(Z)||0;if(q>0)return this.#t(j,q,Q,W,$,G);return this.#N(Q,W,$,j,G)}#N(j,Q,W,X,Z){let $=this.#Ej(W,X),G=this.#Fj(j,X,Z);return this.queue=(this.queue??Promise.resolve()).then(()=>this.#Gj(j,Q,$,G)),this.queue}#a(j,Q,W,X,Z,$){this.#C(j);let G=()=>{this.#C(j),this.#N(W,X,Z,j,$)},J=setTimeout(G,Q);j?.addEventListener?.("blur",G,{once:!0}),this.#V.set(j,{timer:J,flush:G})}#C(j){let Q=this.#V.get(j);if(!Q)return;clearTimeout(Q.timer),j?.removeEventListener?.("blur",Q.flush),this.#V.delete(j)}#r(){for(let j of[...this.#V.keys()])this.#C(j)}#t(j,Q,W,X,Z,$){let G=this.#K.get(j)??new Map;if(G.has(W))return;let J=setTimeout(()=>{if(G.delete(W),G.size===0)this.#K.delete(j)},Q);return G.set(W,J),this.#K.set(j,G),this.#N(W,X,Z,j,$)}#e(){for(let j of this.#K.values())for(let Q of j.values())clearTimeout(Q);this.#K.clear()}#A(j,Q,{cancelable:W=!1}={}){let X=new CustomEvent(j,{bubbles:!0,composed:!0,cancelable:W,detail:Q});return(this.element.isConnected?this.element:document).dispatchEvent(X),X}#X(j,Q,W,X){let Z=()=>{if(!this.element.isConnected){console.warn("[phlex-reactive] retry() ignored — the reactive root left the DOM");return}return this.#N(j,Q)};this.#A("reactive:error",{action:j,params:W,...X,retry:Z})}#Z(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#jj(){this.element?.removeAttribute?.("data-reactive-error")}#Qj(){let j=document.querySelector("[data-reactive-error-flash]");if(!j?.content)return;let Q=j.getAttribute("data-reactive-error-flash")||"flash",W=document.getElementById(Q);if(!W)return;W.appendChild(j.content.cloneNode(!0))}#Wj(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(D));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!A)A=!0,console.warn(`[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${j}ms. Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.`);return new Promise((Q)=>setTimeout(Q,j))}#Xj(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#b(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#Zj(j){if(!j)return[];let Q=[],W=/<turbo-stream\b([^>]*)>/g,X;while((X=W.exec(j))!==null){let Z=X[1],$=Z.match(/\baction="([^"]*)"/)?.[1]??"?",G=Z.match(/\btarget="([^"]*)"/)?.[1];Q.push(G?`${$} → #${G}`:$)}return Q}#$j(j){let{action:Q,status:W,ms:X}=j,$=`reactive ${this.element?.id?`#${this.element.id} `:""}${Q} → ${W??"—"} (${Math.round(X)}ms)`;if(console.groupCollapsed($),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#Gj(j,Q,W,X){let{fields:Z,files:$}=this.#Kj(),G=this.#m(Q),J={...Z,...G},K=this.#$,q=$.length>0,H=q?this.#Lj(K,j,J,$):JSON.stringify({token:K,act:j,params:J}),z=this.#Xj()?{action:j,paramNames:Object.keys(G),fieldNames:Object.keys(Z),encoding:q?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#b()}:null;await this.#Wj();try{if(navigator.onLine===!1){this.#G(W),this.#Z("offline"),this.#X(j,Q,J,{kind:"offline"});return}let Y;try{let V={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#bj()};if(!q)V["Content-Type"]="application/json";let N=this.#hj();if(N)V["X-Pgbus-Connection"]=N;Y=await fetch(this.#kj(),{method:"POST",headers:V,body:H,credentials:"same-origin",signal:AbortSignal.timeout(this.#wj())})}catch(V){if(console.error("[phlex-reactive] action error",V),this.#G(W),V?.name==="TimeoutError"||V?.name==="AbortError"){this.#Z("timeout"),this.#X(j,Q,J,{kind:"timeout"});return}this.#Qj(),this.#Z("network"),this.#X(j,Q,J,{kind:"network"});return}if(z)z.status=Y.status;if(Y.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#G(W),this.#Z("redirected"),this.#X(j,Q,J,{kind:"redirected",status:Y.status});return}if(!Y.ok){let V=await Y.text();if(console.error(`[phlex-reactive] action failed: HTTP ${Y.status}`,V),this.#G(W),(Y.headers.get("Content-Type")||"").includes("turbo-stream")){let N=this.#y(V);if(this.#$=N??this.#$,z)this.#h(z,V,N);window.Turbo.renderStreamMessage(V)}this.#Z("http"),this.#X(j,Q,J,{kind:"http",status:Y.status,body:V});return}let _=Y.headers.get("Content-Type")||"";if(!_.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${_}" — no update applied`),this.#G(W),this.#Z("content-type"),this.#X(j,Q,J,{kind:"content-type",status:Y.status});return}let M=await Y.text(),T=this.#y(M);if(this.#$=T??this.#$,z)this.#h(z,M,T);window.Turbo.renderStreamMessage(M),this.#jj(),this.#A("reactive:applied",{action:j,params:J,html:M})}catch(Y){console.error("[phlex-reactive] action error",Y),this.#G(W),this.#A("reactive:error",{action:j,params:J,kind:"apply"})}finally{if(X?.(),z)this.#$j({...z,ms:this.#b()-z.started})}}#h(j,Q,W){j.streams=this.#Zj(Q),j.tokenRefreshed=W!=null}get#$(){return this.#R??this.tokenValue}set#$(j){this.#R=j}#y(j){let Q=this.element.id;if(!Q)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:W,self:X}=this.#Jj(Q),Z=j.match(W);if(Z)return Z[1];let $=j.match(X);if($)return $[1].match(/data-reactive-token-value="([^"]+)"/)?.[1];return}#Jj(j){let Q=this.#D;if(Q&&Q.id===j)return Q;let W=$j(j);return this.#D={id:j,token:new RegExp(`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${W}"[^>]*\\bdata-reactive-token-value="([^"]+)"`),self:new RegExp(`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${W}"[^>]*>([\\s\\S]*?)</turbo-stream>`)}}#Q(j){return j.closest('[data-controller~="reactive"]')===this.element}#U(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(Q)=>this.#Q(Q)}#Kj(){let j={},Q=[],W=this.#U();return this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((X)=>{if(!W(X))return;if(X.type==="file")for(let Z of X.files??[])Q.push({name:X.name,file:Z,multiple:X.multiple});else if(X.type==="checkbox")j[X.name]=X.checked;else if(X.type==="radio"){if(X.checked)j[X.name]=X.value}else j[X.name]=X.value}),this.element.querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])").forEach((X)=>{if(!W(X))return;let Z=X.getAttribute("name");if(!Z)return;let $=j[Z];if($==null||$==="")j[Z]=X.value??X.textContent??X.innerHTML??""}),{fields:j,files:Q}}#E(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Q)=>{if(!this.#Q(Q))return;if(Q.type==="file")return;if(this.#zj(Q))Q.setAttribute("data-reactive-dirty","true"),j++;else Q.removeAttribute("data-reactive-dirty")}),j>0)this.element.setAttribute("data-reactive-dirty",String(j));else this.element.removeAttribute("data-reactive-dirty")}#zj(j){if(j.type==="checkbox"||j.type==="radio")return j.checked!==j.defaultChecked;if(j.tag==="select"||j.options)return Array.from(j.options??[]).some((Q)=>Q.selected!==Q.defaultSelected);return j.value!==j.defaultValue}#v(){let j=this.element.getAttribute?.("data-reactive-dirty"),Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:0}#Yj(){if(typeof window>"u"||typeof window.addEventListener!=="function")return;this.#q=(j)=>{if(this.#v()===0)return;return j.preventDefault(),j.returnValue="You have unsaved changes.",j.returnValue},this.#H=(j)=>{if(this.#v()===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.#H)}#qj(){if(this.#Y)this.element.removeEventListener?.("turbo:morph-element",this.#Y),this.#Y=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#q)window.removeEventListener("beforeunload",this.#q);if(this.#H)window.removeEventListener("turbo:before-visit",this.#H)}this.#q=void 0,this.#H=void 0}#Hj(){if(this.element.getAttribute?.("data-reactive-show-targets"))return!0;let j=this.element.querySelectorAll?.("[data-reactive-show-field]")??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}#f(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.#U(),Q=new Map;for(let W of this.element.querySelectorAll("[data-reactive-show-field]")){if(!j(W))continue;let X=W.getAttribute("data-reactive-show-field");if(!X)continue;if(!Q.has(X))Q.set(X,this.#u(X,j));let Z=Q.get(X);if(Z===null)continue;let $=qj(W,Z);if($===null)continue;W.hidden=!$}this.#Uj(j,Q)}#Uj(j,Q){let W=this.#Vj();for(let[X,Z]of Object.entries(W)){if(!Z||typeof Z!=="object"||Array.isArray(Z))continue;if(!Q.has(X))Q.set(X,this.#u(X,j));let $=Q.get(X);if($===null)continue;for(let[G,J]of Object.entries(Z)){if(!Uj(G))continue;let K=Hj(J,$);if(K===null){console.warn(`[phlex-reactive] malformed reactive_show_targets predicate for ${G} — skipped`);continue}for(let q of document.querySelectorAll(G))q.hidden=!K}}}#Vj(){let j=this.element.getAttribute?.("data-reactive-show-targets");if(!j)return{};try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn("[phlex-reactive] malformed data-reactive-show-targets — ignored. "+"Did two reactive_show_targets calls collide on one root? Declare every field in ONE call: reactive_show_targets(mode: { ... }, kind: { ... })"),{}}#u(j,Q){let W=!1,X=null;for(let Z of this.element.querySelectorAll(`[name="${j}"]`)){if(!Q(Z))continue;if(Z.type==="checkbox")return Z.checked?"true":"false";if(Z.type==="radio"){if(Z.checked)return Z.value??"";W=!0;continue}X??=Z}if(X)return X.value??"";return W?"":null}#_j(){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}#Mj(){return!!(this.element.getAttribute?.("data-reactive-filter-input")&&this.element.getAttribute?.("data-reactive-filter-option"))}#Nj(j){let Q=this.element.getAttribute("data-reactive-filter-input");return!!Q&&typeof j.target?.matches==="function"&&j.target.matches(Q)}#p(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.element.getAttribute("data-reactive-filter-input"),Q=this.element.getAttribute("data-reactive-filter-option");if(!j||!Q)return;let W=this.#U(),X=[...this.element.querySelectorAll(j)].find(W);if(!X)return;let Z=(X.value??"").trim().toLowerCase(),$=0;for(let K of this.element.querySelectorAll(Q)){if(!W(K))continue;let q=(K.getAttribute("data-reactive-filter-text")??K.textContent??"").toLowerCase(),H=Z!==""&&!q.includes(Z);if(K.hidden=H,H)K.removeAttribute("data-reactive-highlighted");else $++}let G=this.element.getAttribute("data-reactive-filter-group");if(G)for(let K of this.element.querySelectorAll(G)){if(!W(K))continue;let q=[...K.querySelectorAll(Q)].filter(W);if(q.length===0)continue;K.hidden=q.every((H)=>H.hidden)}let J=this.element.getAttribute("data-reactive-filter-empty");if(J){for(let K of this.element.querySelectorAll(J))if(W(K))K.hidden=$>0}}#Aj(){if(!this.#W)return;this.element.removeEventListener?.("input",this.#W),this.element.removeEventListener?.("turbo:morph-element",this.#W),this.#W=void 0}#Lj(j,Q,W,X){let Z=new FormData;Z.append("token",j),Z.append("act",Q);for(let[G,J]of Object.entries(W))this.#P(Z,`params[${G}]`,J);let $=this.#Oj(X);for(let{name:G,file:J,multiple:K}of X){let H=K||$.has(G)?`params[${G}][]`:`params[${G}]`;Z.append(H,J,J.name)}return Z}#P(j,Q,W){if(W==null)j.append(Q,"");else if(Array.isArray(W))W.forEach((X,Z)=>this.#P(j,`${Q}[${Z}]`,X));else if(typeof W==="object")for(let[X,Z]of Object.entries(W))this.#P(j,`${Q}[${X}]`,Z);else j.append(Q,String(W))}#Oj(j){let Q=new Map;for(let{name:W}of j)Q.set(W,(Q.get(W)??0)+1);return new Set([...Q].filter(([,W])=>W>1).map(([W])=>W))}#m(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#xj(j){return f(j)}#Bj(j){u(j,(Q)=>this.#F(Q))}#F(j){let Q=j.to;if(Q==="@root")return[this.element];if(typeof Q!=="string"||Q==="")return[];if(j.global)return[...document.querySelectorAll(Q)];return[...this.element.querySelectorAll(Q)].filter((W)=>this.#Q(W))}#Cj(j,Q){if(j?.checked!=="keep")return!1;let W=Q?.type;return W==="checkbox"||W==="radio"}#Ej(j,Q){if(!j)return null;let W=this.#Pj(j,Q),X=[];for(let Z of W){if(j.add_class){let $=j.add_class.filter((G)=>!Z.classList.contains(G));if(Z.classList.add(...$),$.length)X.push(()=>Z.classList.remove(...$))}if(j.remove_class){let $=j.remove_class.filter((G)=>Z.classList.contains(G));if(Z.classList.remove(...$),$.length)X.push(()=>Z.classList.add(...$))}if(j.toggle_class)j.toggle_class.forEach(($)=>Z.classList.toggle($)),X.push(()=>j.toggle_class.forEach(($)=>Z.classList.toggle($)));if(j.hide)Z.hidden=!0,X.push(()=>Z.hidden=!1)}if(j.checked==="keep"&&Q&&"checked"in Q){let Z=Q.checked;X.push(()=>Q.checked=!Z)}return X.length?X:null}#G(j){if(!j)return;if(!this.element.isConnected)return;for(let Q of j)Q()}#Pj(j,Q){if(j.to==null)return Q?[Q]:[];return this.#F({to:j.to})}#Fj(j,Q,W){this.#Rj(j,Q);let X=this.#Tj(j,Q,W),Z=!1;return()=>{if(Z)return;Z=!0,this.#Dj(j,Q),X()}}#Rj(j,Q){if(this.#J(Q,j,1),this.#J(this.element,j,1),this.#z.set(j,(this.#z.get(j)??0)+1),this.#O++===0)this.element.setAttribute("aria-busy","true");for(let W of this.#c(j))this.#J(W,j,1)}#Dj(j,Q){this.#J(Q,j,-1),this.#J(this.element,j,-1);let W=(this.#z.get(j)??1)-1;if(W<=0)this.#z.delete(j);else this.#z.set(j,W);if(--this.#O<=0)this.#O=0,this.element.removeAttribute("aria-busy");for(let X of this.#c(j))this.#J(X,j,-1)}#J(j,Q,W){if(!j||typeof j.getAttribute!=="function")return;let X=this.#x.get(j)??new Map,Z=(X.get(Q)??0)+W;if(Z<=0)X.delete(Q);else X.set(Q,Z);if(X.size===0){this.#x.delete(j),j.removeAttribute("data-reactive-busy");return}this.#x.set(j,X),j.setAttribute("data-reactive-busy",[...X.keys()].join(" "))}#c(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((W)=>W.getAttribute("data-reactive-busy-on")===j&&this.#Q(W))}#Tj(j,Q,W){if(!W||!Q)return()=>{};let X=this.#Sj(W,Q),Z=Array.isArray(W.class)?W.class:[],$=[];for(let J of X){let K=Z.filter((q)=>!J.classList.contains(q));if(J.classList.add(...K),K.length)$.push([J,K])}let G=this.#_.get(Q);if(G)G.count++;else if(W.disable||W.text!=null)this.#_.set(Q,{count:1,disabled:Q.disabled,text:Q.textContent,hadText:W.text!=null});if(W.disable)Q.disabled=!0;if(W.text!=null)Q.textContent=W.text;return()=>{for(let[J,K]of $)if(J.isConnected)J.classList.remove(...K);this.#Ij(Q,W)}}#Ij(j,Q){let W=this.#_.get(j);if(!W)return;if(--W.count>0)return;if(this.#_.delete(j),!j.isConnected)return;if(Q.disable)j.disabled=W.disabled;if(W.hadText&&j.textContent===Q.text)j.textContent=W.text}#Sj(j,Q){if(j.to==null)return Q?[Q]:[];return this.#F({to:j.to})}#kj(){return this.#g??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#wj(){if(this.#L!=null)return this.#L;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return this.#L=Number.isFinite(Q)&&Q>0?Q:30000}#bj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}#hj(){return document.querySelector("pgbus-stream-source[connection-id]")?.getAttribute("connection-id")||document.querySelector('meta[name="pgbus-connection-id"]')?.content||null}}export{Oj as resetReactiveDefers,g as registerReactiveVisit,d as registerReactiveToken,Qj as registerReactiveOffline,l as registerReactiveJs,e as registerReactiveDismiss,s as registerReactiveDefer,S as registerReactiveActions,xj as pendingDeferVia,$j as escapeRegExp,Wj as enableLatencySim,Xj as disableLatencySim,Rj as default,Gj as checkReactiveRegistration,Pj as __resetReactiveRegistrationForTest,Cj as __resetReactiveOfflineForTest,Ej as __resetReactiveLatencyForTest,Bj as __resetReactiveDismissForTest,Fj as __markReactiveConnectedForTest,D as LATENCY_KEY};
1
+ import{Controller as d}from"@hotwired/stimulus";import{confirmResolver as l}from"phlex/reactive/confirm";import{computeReducer as s}from"phlex/reactive/compute";function o(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:visit"])return;j["reactive:visit"]=function(){let Q=this.getAttribute("data-url");if(Q)window.Turbo.visit(Q,{action:"advance"})}}function n(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:token"])return;j["reactive:token"]=function(){let Q=this.getAttribute("data-reactive-token-value"),X=this.getAttribute("target");if(!Q||!X)return;let Z=document.getElementById(X);if(Z)Z.setAttribute("data-reactive-token-value",Q)}}function i(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:js"])return;j["reactive:js"]=function(){let Q=c(this.getAttribute("data-reactive-ops"));if(!Q.length)return;let X=this.getAttribute("target"),Z=X?document.getElementById(X):null;if(X&&!Z)return;g(Q,($)=>Oj($,Z))}}var H=new Map;function Ej(){H.clear(),E=!1}function Fj(j){return H.get(j)?.via}var E=!1;function a(){let j=window.Turbo?.StreamActions;if(!j||j["reactive:defer"])return;if(j["reactive:defer"]=function(){let Q=this.getAttribute("target");if(!Q)return;if(this.getAttribute("data-reactive-defer-via")==="stream"){t(Q,this);return}let X=this.getAttribute("data-reactive-defer-token");if(!X)return;L(Q,X)},!E&&typeof document<"u"&&document.addEventListener)E=!0,document.addEventListener("turbo:before-stream-render",r)}function r(j){let X=j.target?.getAttribute?.("target");if(!X)return;let Z=X.startsWith("reactive-defer-src-")?X.slice(19):X;if(H.get(Z)?.via==="stream")H.delete(Z)}function L(j,Q){let X=document.getElementById(j);if(!X){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}h(j),y(X);let Z={via:"fetch",abort:new AbortController,timedOut:!1};H.set(j,Z),e(j,Z,Q)}function t(j,Q){let X=document.getElementById(j);if(!X){console.warn(`[phlex-reactive] reactive:defer target #${j} is not on the page — skipped`);return}let Z=Q.getAttribute("data-reactive-defer-src");if(!Z)return;if(!globalThis.customElements?.get?.("pgbus-stream-source")){let J=Q.getAttribute("data-reactive-defer-token");if(J){L(j,J);return}console.error("[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered "+"and no fallback token was provided — is the pgbus client loaded on this page?");return}h(j),y(X);let $=document.createElement("pgbus-stream-source");$.id=w(j),$.setAttribute("src",Z),$.setAttribute("since-id",Q.getAttribute("data-reactive-defer-since-id")??"0"),$.setAttribute("hidden",""),document.body.appendChild($),H.set(j,{via:"stream"})}function w(j){return`reactive-defer-src-${j}`}async function e(j,Q,X){let Z=setTimeout(()=>{Q.timedOut=!0,Q.abort.abort()},Xj()),$;try{$=await fetch(jj(),{method:"POST",headers:{Accept:"text/vnd.turbo-stream.html","Content-Type":"application/json","X-CSRF-Token":Qj()},body:JSON.stringify({token:X}),credentials:"same-origin",signal:Q.abort.signal})}catch(G){if(clearTimeout(Z),H.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed",G),O(j,X);return}if(H.get(j)!==Q){clearTimeout(Z);return}if($.status===204){clearTimeout(Z),I(j);return}if(!$.ok){clearTimeout(Z),console.error(`[phlex-reactive] deferred render failed: HTTP ${$.status}`),O(j,X,$.status);return}let J;try{J=await $.text()}catch(G){if(clearTimeout(Z),H.get(j)!==Q)return;console.error("[phlex-reactive] deferred render failed reading the body",G),O(j,X);return}if(clearTimeout(Z),H.get(j)!==Q)return;I(j),window.Turbo.renderStreamMessage(J)}function h(j){let Q=H.get(j);if(!Q)return;if(H.delete(j),Q.via==="fetch")Q.abort.abort();else document.getElementById(w(j))?.remove?.()}function y(j){j.setAttribute("data-reactive-defer-pending","true"),j.setAttribute("aria-busy","true")}function v(j){j.removeAttribute("data-reactive-defer-pending"),j.removeAttribute("aria-busy")}function I(j){H.delete(j);let Q=document.getElementById(j);if(!Q)return;v(Q),Q.removeAttribute("data-reactive-error")}function O(j,Q,X){H.delete(j);let Z=document.getElementById(j);if(!Z)return;v(Z),Z.setAttribute("data-reactive-error","defer");let $=()=>{let J=document.getElementById(j);if(!J){console.warn("[phlex-reactive] defer retry() ignored — the target left the DOM");return}J.removeAttribute("data-reactive-error"),L(j,Q)};Z.dispatchEvent(new CustomEvent("reactive:error",{bubbles:!0,composed:!0,detail:{kind:"defer",target:j,status:X,retry:$}}))}function jj(){return document.querySelector('meta[name="phlex-reactive-defer-path"]')?.content||"/reactive/defer"}function Qj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}function Xj(){let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:30000}var F=!1;function Zj(){if(F)return;if(typeof document>"u"||!document.addEventListener)return;F=!0,document.addEventListener("turbo:before-stream-render",$j)}function $j(j){let Q=j.detail,X=Q?.render;if(typeof X!=="function"||X.__reactiveDismissWrapped){if(typeof requestAnimationFrame==="function")requestAnimationFrame(B);else setTimeout(B,0);return}let Z=async($)=>{await X($),B()};Z.__reactiveDismissWrapped=!0,Q.render=Z}function B(){let j=document.querySelectorAll("[data-reactive-dismiss-after]");for(let Q of j){if(Q.hasAttribute("data-reactive-dismiss-scheduled"))continue;let X=Number(Q.getAttribute("data-reactive-dismiss-after"));if(!Number.isFinite(X)||X<=0)continue;Q.setAttribute("data-reactive-dismiss-scheduled",""),setTimeout(()=>Q.remove(),X)}}function Rj(){F=!1}var R=!1;function Jj(){if(R)return;if(typeof window>"u"||typeof document>"u")return;if(typeof window.addEventListener!=="function")return;R=!0;let j=()=>{let Q=document.documentElement;if(typeof Q?.toggleAttribute!=="function")return;Q.toggleAttribute("data-reactive-offline",globalThis.navigator?.onLine===!1)};j(),window.addEventListener("online",j),window.addEventListener("offline",j)}function Dj(){R=!1}var D="phlex-reactive:latency",N=!1;function Gj(j){if(typeof sessionStorage>"u")return;sessionStorage.setItem(D,String(j))}function Wj(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem(D),N=!1}function zj(){if(typeof window>"u"||typeof document>"u")return;if(document.querySelector?.('meta[name="phlex-reactive-env"]')?.content!=="development")return;window.PhlexReactive={enableLatencySim:Gj,disableLatencySim:Wj}}function Tj(){N=!1}function S(){o(),n(),i(),a(),Zj(),Jj(),zj()}function Kj(j){return j.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}if(typeof window<"u")if(window.Turbo)S();else document.addEventListener("turbo:load",S,{once:!0});var x=!1;function qj(){if(x)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 Ij(){x=!1}function Sj(){x=!0}if(typeof window<"u"&&typeof document<"u"){let j=()=>setTimeout(qj,0);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",j,{once:!0});else j()}var Yj=new Set(["href","src","srcdoc","action","formaction","xlink:href","style"]);function Uj(j){let Q=String(j).toLowerCase();return Q.startsWith("on")||Yj.has(Q)}function Hj(j,Q,X){let[Z,$,J]=Q;j.classList.add(Z,$),X(),requestAnimationFrame(()=>{j.classList.remove($),j.classList.add(J)});let G=!1,W=()=>{if(G)return;G=!0,j.classList.remove(Z,J)};j.addEventListener("animationend",W,{once:!0}),setTimeout(W,350)}var k=Object.freeze({show:(j,Q)=>C(j,!1,Q),hide:(j,Q)=>C(j,!0,Q),toggle:(j,Q)=>C(j,!j.hidden,Q),add_class:(j,Q)=>j.classList.add(...Q.classes??[]),remove_class:(j,Q)=>j.classList.remove(...Q.classes??[]),toggle_class:(j,Q)=>(Q.classes??[]).forEach((X)=>j.classList.toggle(X)),set_attr:(j,Q)=>{if(P(Q.name))j.setAttribute(Q.name,Q.value??"")},remove_attr:(j,Q)=>{if(P(Q.name))j.removeAttribute(Q.name)},toggle_attr:(j,Q)=>{if(!P(Q.name))return;if(j.hasAttribute(Q.name))j.removeAttribute(Q.name);else j.setAttribute(Q.name,"")},focus:(j)=>j.focus?.(),focus_first:(j)=>xj(j)?.focus?.(),text:(j,Q)=>{let X=String(Q.value??"");if(j.textContent!==X)j.textContent=X},dispatch:(j,Q)=>{j.dispatchEvent(new CustomEvent(Q.name,{bubbles:!0,composed:!0,detail:Q.detail??{}}))}});function C(j,Q,X){if(X?.transition)Hj(j,X.transition,()=>j.hidden=Q);else j.hidden=Q}function P(j){if(!Uj(j))return!0;return console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(j)} — skipped`),!1}var f=/^#[A-Za-z_][\w-]*$/;function Vj(j){if(typeof j==="string"&&f.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(j)} — skipped`),!1}function Aj(j,Q){let X=j.getAttribute("data-reactive-show-equals");if(X!==null)return Q===X;let Z=j.getAttribute("data-reactive-show-not");if(Z!==null)return Q!==Z;let $=j.getAttribute("data-reactive-show-in");if($!==null){try{let J=JSON.parse($);if(Array.isArray(J))return J.includes(Q)}catch{}return console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify($)} — skipped`),null}for(let J of u){let G=j.getAttribute(`data-reactive-show-${J}`);if(G!==null)return p(J,G,Q)}return console.warn("[phlex-reactive] a reactive_show binding declares no predicate — skipped"),null}var u=["gte","gt","lte","lt"];function p(j,Q,X){let Z=Number(Q);if(Number.isNaN(Z))return console.warn(`[phlex-reactive] reactive_show ${j}: needs a numeric literal, got ${JSON.stringify(Q)} — skipped`),null;let $=X==null?"":String(X).trim(),J=$===""?NaN:Number($);if(Number.isNaN(J))return!1;switch(j){case"gte":return J>=Z;case"gt":return J>Z;case"lte":return J<=Z;case"lt":return J<Z;default:return null}}function m(j,Q){if(!j||typeof j!=="object")return null;if(typeof j.equals==="string")return Q===j.equals;if(typeof j.not==="string")return Q!==j.not;if(Array.isArray(j.in))return j.in.includes(Q);for(let X of u)if(X in j)return p(X,j[X],Q);return null}var b="[data-reactive-show-field], [data-reactive-show]";function _j(j){try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn(`[phlex-reactive] malformed compound reactive_show payload ${JSON.stringify(j)} — skipped`),null}function Mj(j,Q){if(!j||typeof j!=="object")return null;let X=Array.isArray(j.all)?"all":Array.isArray(j.any)?"any":null;if(!X)return null;let Z=j[X];if(Z.length===0)return null;let $=Z.map((J)=>{if(!J||typeof J!=="object"||typeof J.field!=="string")return!1;let G=Q(J.field);if(G===null)return!1;return m(J,G)===!0});return X==="all"?$.every(Boolean):$.some(Boolean)}function Nj(j){if(typeof j==="string"&&f.test(j))return!0;return console.warn(`[phlex-reactive] refused cross-root show target ${JSON.stringify(j)} — skipped`),!1}var Lj='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function xj(j){return j.querySelectorAll?.(Lj)?.[0]??null}function c(j){if(Array.isArray(j))return j;if(typeof j!=="string")return[];try{let Q=JSON.parse(j);return Array.isArray(Q)?Q:[]}catch{return[]}}function g(j,Q){for(let X of j){if(!Array.isArray(X))continue;let[Z,$={}]=X;if(!Object.hasOwn(k,Z)){console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(Z)} — skipped`);continue}for(let J of Q($))k[Z](J,$)}}function Oj(j,Q){let X=j.to;if(Q){if(X==="@root")return[Q];if(typeof X!=="string"||X==="")return[];if(j.global)return[...document.querySelectorAll(X)];return[...Q.querySelectorAll(X)]}if(typeof X!=="string"||X===""||X==="@root")return[];return[...document.querySelectorAll(X)]}class kj extends d{static values={token:String};#R;#V=new Map;#z=new Map;#g;#L;#D;#x=0;#K=new Map;#O=new WeakMap;#A=new Map;#q;#Y;#U;#j;#X;#_;connect(){if(x=!0,this.element.id==="")console.warn("[phlex-reactive] a reactive root has no id; its next-action token can't self-match "+"and may fall back to the first token in the response → a silent HTTP 403 on the NEXT action. "+"Put id: on the SAME element as reactive_attrs — use div(**reactive_root) (emits id + token together), "+"or div(id:, **reactive_attrs). The id: must NOT be on a child. See the README.");if(this.element.getAttribute?.("data-reactive-defer-token"))this.#T(),this.#_=()=>this.#T(),this.element.addEventListener?.("turbo:morph-element",this.#_);if(this.#d()){if(this.#q=()=>this.#P(),this.element.addEventListener?.("turbo:morph-element",this.#q),this.#P(),this.element.getAttribute?.("data-reactive-warn-unsaved")==="true")this.#qj()}if(this.#Uj())this.#j=()=>this.#f(),this.element.addEventListener?.("input",this.#j),this.element.addEventListener?.("change",this.#j),this.element.addEventListener?.("turbo:morph-element",this.#j),this.#f();if(this.#_j())this.#X=(j)=>{if(j?.type==="input"&&!this.#Mj(j))return;this.#p()},this.element.addEventListener?.("input",this.#X),this.element.addEventListener?.("turbo:morph-element",this.#X),this.#p()}#d(){if((this.element.getAttribute?.("data-action")??"").includes("reactive#trackDirty"))return!0;let j=this.element.querySelectorAll?.('[data-action*="reactive#trackDirty"]')??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}disconnect(){if(this.#r(),this.#e(),this.#Yj(),this.#Aj(),this.#Nj(),this.#_)this.element.removeEventListener?.("turbo:morph-element",this.#_)}#T(){let j=this.element;if(!j?.id)return;let Q=j.getAttribute?.("data-reactive-defer-token");if(!Q)return;if(j.getAttribute?.("data-reactive-defer-pending")!=="true")return;L(j.id,Q)}dispatch(j){let{action:Q,params:X,debounce:Z,throttle:$,confirm:J,outside:G,window:W,optimistic:z,loading:Y}=j.params;if(!Q)return;if(G&&this.element.contains(j.target))return;let U=j.currentTarget??j.target;if(!W&&!this.#Cj(z,U))j.preventDefault();if(!J)return this.#b(U,Q,X,Z,$,z,Y);Promise.resolve().then(()=>l(J)).catch(()=>!1).then((K)=>{if(K)this.#b(U,Q,X,Z,$,z,Y)})}runOps(j){let{ops:Q,outside:X,window:Z}=j.params;if(X&&this.element.contains(j.target))return;if(!Z)j.preventDefault();this.#Bj(this.#Oj(Q))}trackDirty(){this.#P()}recompute(j){let Q=this.#s(),X=Q.map(([K])=>K),Z=this.#H(),$=new Map,J=(K)=>{if($.has(K))return $.get(K);let q=null;for(let A of this.element.querySelectorAll(`[name="${K}"]`))if(Z(A)){q=A;break}return $.set(K,q),q};for(let K of X)this.#S(K,J(K)?.value??"");let G=this.element.getAttribute("data-reactive-compute-reducer-param"),W=G?s(G):null;if(!W){this.#k({},J);return}let z=this.#l("data-reactive-compute-outputs-param"),Y={};for(let[K,q]of Q){let A=J(K);if(q==="string")Y[K]=A?.value??"";else{let _=Number(A?.value);Y[K]=Number.isFinite(_)?_:0}}let U=W(Y,{changed:this.#o(j,X)})||{};for(let K of z){if(!(K in U))continue;let q=J(K);if(q){if(String(U[K])===q.value)continue;q.value=U[K],q.dispatchEvent(new Event("input",{bubbles:!0}))}else this.#S(K,U[K])}this.#k(U,J)}listnavNext(j){this.#I(j,1)}listnavPrev(j){this.#I(j,-1)}listnavPick(j){let Q=this.#B(j),X=Q.findIndex((Z)=>Z.hasAttribute("data-reactive-highlighted"));if(X<0)return;j.preventDefault(),Q[X].click()}listnavClose(j){for(let Q of this.#B(j))Q.removeAttribute("data-reactive-highlighted")}#I(j,Q){let X=this.#B(j);if(!X.length)return;j.preventDefault();let Z=X.findIndex((G)=>G.hasAttribute("data-reactive-highlighted")),$=Z<0?Q>0?0:X.length-1:(Z+Q+X.length)%X.length;for(let G of X)G.removeAttribute("data-reactive-highlighted");let J=X[$];J.setAttribute("data-reactive-highlighted","true"),J.scrollIntoView?.({block:"nearest"})}#B(j){let X=(j?.currentTarget??j?.target??this.element).getAttribute?.("data-reactive-listnav-option-param")??this.element.getAttribute("data-reactive-listnav-option-param");if(!X)return[];let Z=this.#H();return Array.from(this.element.querySelectorAll(X)).filter(($)=>!$.hidden&&Z($))}#l(j){let Q=this.element.getAttribute(j);if(!Q)return[];try{let X=JSON.parse(Q);return Array.isArray(X)?X:[]}catch{return[]}}#s(){let j=this.element.getAttribute("data-reactive-compute-inputs-param");if(!j)return[];try{let Q=JSON.parse(j);if(Array.isArray(Q))return Q.map((X)=>[X,"number"]);if(Q&&typeof Q==="object")return Object.entries(Q);return[]}catch{return[]}}#o(j,Q){let X=j?.target;if(!X?.name||typeof X.closest!=="function")return null;if(!Q.includes(X.name))return null;return this.#Q(X)?X.name:null}#S(j,Q){let X=String(Q);for(let Z of this.#n(j)){if(Z.textContent===X)continue;Z.textContent=X}}#n(j){let Q=this.element.querySelectorAll(`[data-reactive-text="${j}"]`);return Array.from(Q).filter((X)=>this.#Q(X))}#k(j,Q){let X=this.#i();for(let[Z,$]of Object.entries(X)){let J=Z in j?j[Z]:Q(Z)?.value;if(J===void 0||J===null)continue;let G=String(J);for(let W of Array.isArray($)?$:[$]){if(!Vj(W))continue;for(let z of document.querySelectorAll(W)){if(z.textContent===G)continue;z.textContent=G}}}}#i(){let j=this.element.getAttribute("data-reactive-compute-mirror-param");if(!j)return{};try{let Q=JSON.parse(j);return Q&&typeof Q==="object"&&!Array.isArray(Q)?Q:{}}catch{return{}}}#b(j,Q,X,Z,$,J,G){if(this.#N("reactive:before-dispatch",{action:Q,params:this.#m(X),element:this.element},{cancelable:!0}).defaultPrevented)return;let z=Number(Z)||0;if(z>0)return this.#a(j,z,Q,X,J,G);let Y=Number($)||0;if(Y>0)return this.#t(j,Y,Q,X,J,G);return this.#M(Q,X,J,j,G)}#M(j,Q,X,Z,$){let J=this.#Pj(X,Z),G=this.#Fj(j,Z,$);return this.queue=(this.queue??Promise.resolve()).then(()=>this.#Gj(j,Q,J,G)),this.queue}#a(j,Q,X,Z,$,J){this.#C(j);let G=()=>{this.#C(j),this.#M(X,Z,$,j,J)},W=setTimeout(G,Q);j?.addEventListener?.("blur",G,{once:!0}),this.#V.set(j,{timer:W,flush:G})}#C(j){let Q=this.#V.get(j);if(!Q)return;clearTimeout(Q.timer),j?.removeEventListener?.("blur",Q.flush),this.#V.delete(j)}#r(){for(let j of[...this.#V.keys()])this.#C(j)}#t(j,Q,X,Z,$,J){let G=this.#z.get(j)??new Map;if(G.has(X))return;let W=setTimeout(()=>{if(G.delete(X),G.size===0)this.#z.delete(j)},Q);return G.set(X,W),this.#z.set(j,G),this.#M(X,Z,$,j,J)}#e(){for(let j of this.#z.values())for(let Q of j.values())clearTimeout(Q);this.#z.clear()}#N(j,Q,{cancelable:X=!1}={}){let Z=new CustomEvent(j,{bubbles:!0,composed:!0,cancelable:X,detail:Q});return(this.element.isConnected?this.element:document).dispatchEvent(Z),Z}#Z(j,Q,X,Z){let $=()=>{if(!this.element.isConnected){console.warn("[phlex-reactive] retry() ignored — the reactive root left the DOM");return}return this.#M(j,Q)};this.#N("reactive:error",{action:j,params:X,...Z,retry:$})}#$(j){if(this.element?.isConnected===!1)return;this.element?.setAttribute?.("data-reactive-error",j)}#jj(){this.element?.removeAttribute?.("data-reactive-error")}#Qj(){let j=document.querySelector("[data-reactive-error-flash]");if(!j?.content)return;let Q=j.getAttribute("data-reactive-error-flash")||"flash",X=document.getElementById(Q);if(!X)return;X.appendChild(j.content.cloneNode(!0))}#Xj(){if(typeof sessionStorage>"u")return Promise.resolve();let j=Number(sessionStorage.getItem(D));if(!Number.isFinite(j)||j<=0)return Promise.resolve();if(!N)N=!0,console.warn(`[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${j}ms. Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.`);return new Promise((Q)=>setTimeout(Q,j))}#Zj(){return this.element?.getAttribute?.("data-reactive-debug")==="true"}#w(){return typeof performance<"u"&&typeof performance.now==="function"?performance.now():Date.now()}#$j(j){if(!j)return[];let Q=[],X=/<turbo-stream\b([^>]*)>/g,Z;while((Z=X.exec(j))!==null){let $=Z[1],J=$.match(/\baction="([^"]*)"/)?.[1]??"?",G=$.match(/\btarget="([^"]*)"/)?.[1];Q.push(G?`${J} → #${G}`:J)}return Q}#Jj(j){let{action:Q,status:X,ms:Z}=j,J=`reactive ${this.element?.id?`#${this.element.id} `:""}${Q} → ${X??"—"} (${Math.round(Z)}ms)`;if(console.groupCollapsed(J),console.log(`params: [${j.paramNames.join(", ")}] + collected: [${j.fieldNames.join(", ")}]`),console.log(`encoding: ${j.encoding}`),j.streams.length)console.log(`streams: ${j.streams.join(" ")}`);console.log(`token: ${j.tokenRefreshed?"refreshed ✓":"unchanged"}`),console.groupEnd()}async#Gj(j,Q,X,Z){let{fields:$,files:J}=this.#zj(),G=this.#m(Q),W={...$,...G},z=this.#J,Y=J.length>0,U=Y?this.#Lj(z,j,W,J):JSON.stringify({token:z,act:j,params:W}),K=this.#Zj()?{action:j,paramNames:Object.keys(G),fieldNames:Object.keys($),encoding:Y?"multipart":"json",status:null,streams:[],tokenRefreshed:!1,started:this.#w()}:null;await this.#Xj();try{if(navigator.onLine===!1){this.#G(X),this.#$("offline"),this.#Z(j,Q,W,{kind:"offline"});return}let q;try{let V={Accept:"text/vnd.turbo-stream.html","X-CSRF-Token":this.#wj()};if(!Y)V["Content-Type"]="application/json";let M=this.#hj();if(M)V["X-Pgbus-Connection"]=M;q=await fetch(this.#kj(),{method:"POST",headers:V,body:U,credentials:"same-origin",signal:AbortSignal.timeout(this.#bj())})}catch(V){if(console.error("[phlex-reactive] action error",V),this.#G(X),V?.name==="TimeoutError"||V?.name==="AbortError"){this.#$("timeout"),this.#Z(j,Q,W,{kind:"timeout"});return}this.#Qj(),this.#$("network"),this.#Z(j,Q,W,{kind:"network"});return}if(K)K.status=q.status;if(q.redirected){console.error("[phlex-reactive] action was redirected (auth/CSRF?) — no update applied"),this.#G(X),this.#$("redirected"),this.#Z(j,Q,W,{kind:"redirected",status:q.status});return}if(!q.ok){let V=await q.text();if(console.error(`[phlex-reactive] action failed: HTTP ${q.status}`,V),this.#G(X),(q.headers.get("Content-Type")||"").includes("turbo-stream")){let M=this.#y(V);if(this.#J=M??this.#J,K)this.#h(K,V,M);window.Turbo.renderStreamMessage(V)}this.#$("http"),this.#Z(j,Q,W,{kind:"http",status:q.status,body:V});return}let A=q.headers.get("Content-Type")||"";if(!A.includes("turbo-stream")){console.error(`[phlex-reactive] expected a turbo-stream, got "${A}" — no update applied`),this.#G(X),this.#$("content-type"),this.#Z(j,Q,W,{kind:"content-type",status:q.status});return}let _=await q.text(),T=this.#y(_);if(this.#J=T??this.#J,K)this.#h(K,_,T);window.Turbo.renderStreamMessage(_),this.#jj(),this.#N("reactive:applied",{action:j,params:W,html:_})}catch(q){console.error("[phlex-reactive] action error",q),this.#G(X),this.#N("reactive:error",{action:j,params:W,kind:"apply"})}finally{if(Z?.(),K)this.#Jj({...K,ms:this.#w()-K.started})}}#h(j,Q,X){j.streams=this.#$j(Q),j.tokenRefreshed=X!=null}get#J(){return this.#R??this.tokenValue}set#J(j){this.#R=j}#y(j){let Q=this.element.id;if(!Q)return j.match(/data-reactive-token-value="([^"]+)"/)?.[1];let{token:X,self:Z}=this.#Wj(Q),$=j.match(X);if($)return $[1];let J=j.match(Z);if(J)return J[1].match(/data-reactive-token-value="([^"]+)"/)?.[1];return}#Wj(j){let Q=this.#D;if(Q&&Q.id===j)return Q;let X=Kj(j);return this.#D={id:j,token:new RegExp(`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${X}"[^>]*\\bdata-reactive-token-value="([^"]+)"`),self:new RegExp(`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${X}"[^>]*>([\\s\\S]*?)</turbo-stream>`)}}#Q(j){return j.closest('[data-controller~="reactive"]')===this.element}#H(){if(this.element.querySelectorAll('[data-controller~="reactive"]').length===0)return()=>!0;return(Q)=>this.#Q(Q)}#zj(){let j={},Q=[],X=this.#H();return this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Z)=>{if(!X(Z))return;if(Z.type==="file")for(let $ of Z.files??[])Q.push({name:Z.name,file:$,multiple:Z.multiple});else if(Z.type==="checkbox")j[Z.name]=Z.checked;else if(Z.type==="radio"){if(Z.checked)j[Z.name]=Z.value}else j[Z.name]=Z.value}),this.element.querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])").forEach((Z)=>{if(!X(Z))return;let $=Z.getAttribute("name");if(!$)return;let J=j[$];if(J==null||J==="")j[$]=Z.value??Z.textContent??Z.innerHTML??""}),{fields:j,files:Q}}#P(){if(typeof this.element?.querySelectorAll!=="function")return;let j=0;if(this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((Q)=>{if(!this.#Q(Q))return;if(Q.type==="file")return;if(this.#Kj(Q))Q.setAttribute("data-reactive-dirty","true"),j++;else Q.removeAttribute("data-reactive-dirty")}),j>0)this.element.setAttribute("data-reactive-dirty",String(j));else this.element.removeAttribute("data-reactive-dirty")}#Kj(j){if(j.type==="checkbox"||j.type==="radio")return j.checked!==j.defaultChecked;if(j.tag==="select"||j.options)return Array.from(j.options??[]).some((Q)=>Q.selected!==Q.defaultSelected);return j.value!==j.defaultValue}#v(){let j=this.element.getAttribute?.("data-reactive-dirty"),Q=Number(j);return Number.isFinite(Q)&&Q>0?Q:0}#qj(){if(typeof window>"u"||typeof window.addEventListener!=="function")return;this.#Y=(j)=>{if(this.#v()===0)return;return j.preventDefault(),j.returnValue="You have unsaved changes.",j.returnValue},this.#U=(j)=>{if(this.#v()===0)return;if(!(typeof window.confirm==="function"?window.confirm("You have unsaved changes. Leave anyway?"):!0))j.preventDefault?.()},window.addEventListener("beforeunload",this.#Y),window.addEventListener("turbo:before-visit",this.#U)}#Yj(){if(this.#q)this.element.removeEventListener?.("turbo:morph-element",this.#q),this.#q=void 0;if(typeof window<"u"&&typeof window.removeEventListener==="function"){if(this.#Y)window.removeEventListener("beforeunload",this.#Y);if(this.#U)window.removeEventListener("turbo:before-visit",this.#U)}this.#Y=void 0,this.#U=void 0}#Uj(){if(this.element.getAttribute?.("data-reactive-show-targets"))return!0;let j=this.element.querySelectorAll?.(b)??[];for(let Q of j)if(this.#Q(Q))return!0;return!1}#f(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.#H(),Q=new Map,X=(Z)=>{if(!Q.has(Z))Q.set(Z,this.#u(Z,j));return Q.get(Z)};for(let Z of this.element.querySelectorAll(b)){if(!j(Z))continue;let $=Z.getAttribute("data-reactive-show");if($!==null){let z=Mj(_j($),X);if(z!==null)Z.hidden=!z;continue}let J=Z.getAttribute("data-reactive-show-field");if(!J)continue;let G=X(J);if(G===null)continue;let W=Aj(Z,G);if(W===null)continue;Z.hidden=!W}this.#Hj(j,Q)}#Hj(j,Q){let X=this.#Vj();for(let[Z,$]of Object.entries(X)){if(!$||typeof $!=="object"||Array.isArray($))continue;if(!Q.has(Z))Q.set(Z,this.#u(Z,j));let J=Q.get(Z);if(J===null)continue;for(let[G,W]of Object.entries($)){if(!Nj(G))continue;let z=m(W,J);if(z===null){console.warn(`[phlex-reactive] malformed reactive_show_targets predicate for ${G} — skipped`);continue}for(let Y of document.querySelectorAll(G))Y.hidden=!z}}}#Vj(){let j=this.element.getAttribute?.("data-reactive-show-targets");if(!j)return{};try{let Q=JSON.parse(j);if(Q&&typeof Q==="object"&&!Array.isArray(Q))return Q}catch{}return console.warn("[phlex-reactive] malformed data-reactive-show-targets — ignored. "+"Did two reactive_show_targets calls collide on one root? Declare every field in ONE call: reactive_show_targets(mode: { ... }, kind: { ... })"),{}}#u(j,Q){let X=!1,Z=null;for(let $ of this.element.querySelectorAll(`[name="${j}"]`)){if(!Q($))continue;if($.type==="checkbox")return $.checked?"true":"false";if($.type==="radio"){if($.checked)return $.value??"";X=!0;continue}Z??=$}if(Z)return Z.value??"";return X?"":null}#Aj(){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}#_j(){return!!(this.element.getAttribute?.("data-reactive-filter-input")&&this.element.getAttribute?.("data-reactive-filter-option"))}#Mj(j){let Q=this.element.getAttribute("data-reactive-filter-input");return!!Q&&typeof j.target?.matches==="function"&&j.target.matches(Q)}#p(){if(typeof this.element?.querySelectorAll!=="function")return;let j=this.element.getAttribute("data-reactive-filter-input"),Q=this.element.getAttribute("data-reactive-filter-option");if(!j||!Q)return;let X=this.#H(),Z=[...this.element.querySelectorAll(j)].find(X);if(!Z)return;let $=(Z.value??"").trim().toLowerCase(),J=0;for(let z of this.element.querySelectorAll(Q)){if(!X(z))continue;let Y=(z.getAttribute("data-reactive-filter-text")??z.textContent??"").toLowerCase(),U=$!==""&&!Y.includes($);if(z.hidden=U,U)z.removeAttribute("data-reactive-highlighted");else J++}let G=this.element.getAttribute("data-reactive-filter-group");if(G)for(let z of this.element.querySelectorAll(G)){if(!X(z))continue;let Y=[...z.querySelectorAll(Q)].filter(X);if(Y.length===0)continue;z.hidden=Y.every((U)=>U.hidden)}let W=this.element.getAttribute("data-reactive-filter-empty");if(W){for(let z of this.element.querySelectorAll(W))if(X(z))z.hidden=J>0}}#Nj(){if(!this.#X)return;this.element.removeEventListener?.("input",this.#X),this.element.removeEventListener?.("turbo:morph-element",this.#X),this.#X=void 0}#Lj(j,Q,X,Z){let $=new FormData;$.append("token",j),$.append("act",Q);for(let[G,W]of Object.entries(X))this.#E($,`params[${G}]`,W);let J=this.#xj(Z);for(let{name:G,file:W,multiple:z}of Z){let U=z||J.has(G)?`params[${G}][]`:`params[${G}]`;$.append(U,W,W.name)}return $}#E(j,Q,X){if(X==null)j.append(Q,"");else if(Array.isArray(X))X.forEach((Z,$)=>this.#E(j,`${Q}[${$}]`,Z));else if(typeof X==="object")for(let[Z,$]of Object.entries(X))this.#E(j,`${Q}[${Z}]`,$);else j.append(Q,String(X))}#xj(j){let Q=new Map;for(let{name:X}of j)Q.set(X,(Q.get(X)??0)+1);return new Set([...Q].filter(([,X])=>X>1).map(([X])=>X))}#m(j){if(!j)return{};try{return typeof j==="string"?JSON.parse(j):j}catch{return{}}}#Oj(j){return c(j)}#Bj(j){g(j,(Q)=>this.#F(Q))}#F(j){let Q=j.to;if(Q==="@root")return[this.element];if(typeof Q!=="string"||Q==="")return[];if(j.global)return[...document.querySelectorAll(Q)];return[...this.element.querySelectorAll(Q)].filter((X)=>this.#Q(X))}#Cj(j,Q){if(j?.checked!=="keep")return!1;let X=Q?.type;return X==="checkbox"||X==="radio"}#Pj(j,Q){if(!j)return null;let X=this.#Ej(j,Q),Z=[];for(let $ of X){if(j.add_class){let J=j.add_class.filter((G)=>!$.classList.contains(G));if($.classList.add(...J),J.length)Z.push(()=>$.classList.remove(...J))}if(j.remove_class){let J=j.remove_class.filter((G)=>$.classList.contains(G));if($.classList.remove(...J),J.length)Z.push(()=>$.classList.add(...J))}if(j.toggle_class)j.toggle_class.forEach((J)=>$.classList.toggle(J)),Z.push(()=>j.toggle_class.forEach((J)=>$.classList.toggle(J)));if(j.hide)$.hidden=!0,Z.push(()=>$.hidden=!1)}if(j.checked==="keep"&&Q&&"checked"in Q){let $=Q.checked;Z.push(()=>Q.checked=!$)}return Z.length?Z:null}#G(j){if(!j)return;if(!this.element.isConnected)return;for(let Q of j)Q()}#Ej(j,Q){if(j.to==null)return Q?[Q]:[];return this.#F({to:j.to})}#Fj(j,Q,X){this.#Rj(j,Q);let Z=this.#Tj(j,Q,X),$=!1;return()=>{if($)return;$=!0,this.#Dj(j,Q),Z()}}#Rj(j,Q){if(this.#W(Q,j,1),this.#W(this.element,j,1),this.#K.set(j,(this.#K.get(j)??0)+1),this.#x++===0)this.element.setAttribute("aria-busy","true");for(let X of this.#c(j))this.#W(X,j,1)}#Dj(j,Q){this.#W(Q,j,-1),this.#W(this.element,j,-1);let X=(this.#K.get(j)??1)-1;if(X<=0)this.#K.delete(j);else this.#K.set(j,X);if(--this.#x<=0)this.#x=0,this.element.removeAttribute("aria-busy");for(let Z of this.#c(j))this.#W(Z,j,-1)}#W(j,Q,X){if(!j||typeof j.getAttribute!=="function")return;let Z=this.#O.get(j)??new Map,$=(Z.get(Q)??0)+X;if($<=0)Z.delete(Q);else Z.set(Q,$);if(Z.size===0){this.#O.delete(j),j.removeAttribute("data-reactive-busy");return}this.#O.set(j,Z),j.setAttribute("data-reactive-busy",[...Z.keys()].join(" "))}#c(j){return[...this.element.querySelectorAll?.("[data-reactive-busy-on]")??[]].filter((X)=>X.getAttribute("data-reactive-busy-on")===j&&this.#Q(X))}#Tj(j,Q,X){if(!X||!Q)return()=>{};let Z=this.#Sj(X,Q),$=Array.isArray(X.class)?X.class:[],J=[];for(let W of Z){let z=$.filter((Y)=>!W.classList.contains(Y));if(W.classList.add(...z),z.length)J.push([W,z])}let G=this.#A.get(Q);if(G)G.count++;else if(X.disable||X.text!=null)this.#A.set(Q,{count:1,disabled:Q.disabled,text:Q.textContent,hadText:X.text!=null});if(X.disable)Q.disabled=!0;if(X.text!=null)Q.textContent=X.text;return()=>{for(let[W,z]of J)if(W.isConnected)W.classList.remove(...z);this.#Ij(Q,X)}}#Ij(j,Q){let X=this.#A.get(j);if(!X)return;if(--X.count>0)return;if(this.#A.delete(j),!j.isConnected)return;if(Q.disable)j.disabled=X.disabled;if(X.hadText&&j.textContent===Q.text)j.textContent=X.text}#Sj(j,Q){if(j.to==null)return Q?[Q]:[];return this.#F({to:j.to})}#kj(){return this.#g??=document.querySelector('meta[name="phlex-reactive-action-path"]')?.content||"/reactive/actions"}#bj(){if(this.#L!=null)return this.#L;let j=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,Q=Number(j);return this.#L=Number.isFinite(Q)&&Q>0?Q:30000}#wj(){return document.querySelector('meta[name="csrf-token"]')?.content??""}#hj(){return document.querySelector("pgbus-stream-source[connection-id]")?.getAttribute("connection-id")||document.querySelector('meta[name="pgbus-connection-id"]')?.content||null}}export{Ej as resetReactiveDefers,o as registerReactiveVisit,n as registerReactiveToken,Jj as registerReactiveOffline,i as registerReactiveJs,Zj as registerReactiveDismiss,a as registerReactiveDefer,S as registerReactiveActions,Fj as pendingDeferVia,Kj as escapeRegExp,Gj as enableLatencySim,Wj as disableLatencySim,kj as default,qj as checkReactiveRegistration,Ij as __resetReactiveRegistrationForTest,Dj as __resetReactiveOfflineForTest,Tj as __resetReactiveLatencyForTest,Rj as __resetReactiveDismissForTest,Sj as __markReactiveConnectedForTest,D as LATENCY_KEY};
2
2
 
3
- //# debugId=40909AB57A30FB9064756E2164756E21
3
+ //# debugId=CA27DF71E8491EB164756E2164756E21
4
4
  //# sourceMappingURL=reactive_controller.min.js.map