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 +4 -4
- data/CHANGELOG.md +15 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +125 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/helpers.rb +169 -27
- 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: 9e6f3431087aff47f32d503174b5242c51af5fce1bbdd7832d40a466a5c3a548
|
|
4
|
+
data.tar.gz: 7bed02791a8b41163bd7b237aa1bb2730ff99496450783eae712bbcb1ae46052
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
3
|
+
//# debugId=CA27DF71E8491EB164756E2164756E21
|
|
4
4
|
//# sourceMappingURL=reactive_controller.min.js.map
|