phlex-reactive 0.13.3 → 0.13.4
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 +47 -0
- data/README.md +78 -3
- data/app/javascript/phlex/reactive/inspect.js +21 -9
- data/app/javascript/phlex/reactive/inspect.min.js +2 -2
- data/app/javascript/phlex/reactive/inspect.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.js +259 -18
- 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 +65 -19
- data/lib/phlex/reactive/engine.rb +1 -0
- data/lib/phlex/reactive/js.rb +44 -10
- data/lib/phlex/reactive/pending/markup.rb +128 -0
- data/lib/phlex/reactive/pending.rb +25 -21
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +2 -1
|
@@ -1524,16 +1524,18 @@ function persistClearRoot(root) {
|
|
|
1524
1524
|
// Frozen so nothing can be registered into it at runtime — extending the
|
|
1525
1525
|
// vocabulary is a gem change, not an app hook.
|
|
1526
1526
|
const CLIENT_OPS = Object.freeze({
|
|
1527
|
-
show: (el, args) =>
|
|
1528
|
-
hide: (el, args) =>
|
|
1529
|
-
toggle: (el, args) =>
|
|
1527
|
+
show: (el, args, resolveTargets) => setVisibility(el, false, args, resolveTargets),
|
|
1528
|
+
hide: (el, args, resolveTargets) => setVisibility(el, true, args, resolveTargets),
|
|
1529
|
+
toggle: (el, args, resolveTargets) => setVisibility(el, !el.hidden, args, resolveTargets),
|
|
1530
1530
|
add_class: (el, args) => el.classList.add(...(args.classes ?? [])),
|
|
1531
1531
|
remove_class: (el, args) => el.classList.remove(...(args.classes ?? [])),
|
|
1532
1532
|
toggle_class: (el, args) => (args.classes ?? []).forEach((c) => el.classList.toggle(c)),
|
|
1533
1533
|
|
|
1534
1534
|
// Attribute ops (issue #96), interpret-time allowlisted. set_attr writes the
|
|
1535
1535
|
// (already-stringified) value; toggle_attr adds a missing attr (value "") or
|
|
1536
|
-
// removes a present one
|
|
1536
|
+
// removes a present one — or, given values [on, off] (issue #271), flips
|
|
1537
|
+
// between them (an absent attr becomes `on`); remove_attr removes it. A
|
|
1538
|
+
// refused name warns + skips.
|
|
1537
1539
|
set_attr: (el, args) => {
|
|
1538
1540
|
if (guardAttr(args.name)) el.setAttribute(args.name, args.value ?? "")
|
|
1539
1541
|
},
|
|
@@ -1542,7 +1544,10 @@ const CLIENT_OPS = Object.freeze({
|
|
|
1542
1544
|
},
|
|
1543
1545
|
toggle_attr: (el, args) => {
|
|
1544
1546
|
if (!guardAttr(args.name)) return
|
|
1545
|
-
if (
|
|
1547
|
+
if (Array.isArray(args.values)) {
|
|
1548
|
+
const [on, off] = args.values
|
|
1549
|
+
el.setAttribute(args.name, el.getAttribute(args.name) === on ? off : on)
|
|
1550
|
+
} else if (el.hasAttribute(args.name)) el.removeAttribute(args.name)
|
|
1546
1551
|
else el.setAttribute(args.name, "")
|
|
1547
1552
|
},
|
|
1548
1553
|
|
|
@@ -1642,6 +1647,18 @@ function setHidden(el, hidden, args) {
|
|
|
1642
1647
|
else el.hidden = hidden
|
|
1643
1648
|
}
|
|
1644
1649
|
|
|
1650
|
+
// show/hide/toggle (issue #271): flip visibility, then mirror the INTENDED
|
|
1651
|
+
// state into the `expanded:` target's aria-expanded — computed before the flip,
|
|
1652
|
+
// so a transition never delays or races it. The expanded target resolves with
|
|
1653
|
+
// the op's own scoping (root-scoped, or document-wide under global: true).
|
|
1654
|
+
function setVisibility(el, hidden, args, resolveTargets) {
|
|
1655
|
+
setHidden(el, hidden, args)
|
|
1656
|
+
if (args?.expanded == null || typeof resolveTargets !== "function") return
|
|
1657
|
+
for (const target of resolveTargets({ ...args, to: args.expanded })) {
|
|
1658
|
+
target.setAttribute?.("aria-expanded", String(!hidden))
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1645
1662
|
// The interpret-time attribute guard: refuse (warn + skip) a name off the
|
|
1646
1663
|
// allowlist. Returns true when the op may proceed.
|
|
1647
1664
|
function guardAttr(name) {
|
|
@@ -1947,6 +1964,168 @@ function parseOps(raw) {
|
|
|
1947
1964
|
}
|
|
1948
1965
|
}
|
|
1949
1966
|
|
|
1967
|
+
// --- on_client binding records (issue #271) -----------------------------------
|
|
1968
|
+
// Each on_client call emits ONE record ({on, ops, window?, outside?, confirm?,
|
|
1969
|
+
// confirmWhen?}); mix space-joins several onto one element (spaces inside a
|
|
1970
|
+
// record ride as \u0020, so the join is unambiguous). Stimulus hands every
|
|
1971
|
+
// runOps descriptor on an element the SAME event.params, so runOps selects the
|
|
1972
|
+
// record(s) whose descriptor matches the firing event: event.type, the key
|
|
1973
|
+
// filter, and window-boundness.
|
|
1974
|
+
|
|
1975
|
+
// Stimulus's default keyMappings (letters and digits map to themselves). A
|
|
1976
|
+
// custom Stimulus schema is not mirrored — an unknown filter never matches.
|
|
1977
|
+
const KEY_FILTER_MAP = Object.freeze({
|
|
1978
|
+
enter: "Enter",
|
|
1979
|
+
tab: "Tab",
|
|
1980
|
+
esc: "Escape",
|
|
1981
|
+
space: " ",
|
|
1982
|
+
up: "ArrowUp",
|
|
1983
|
+
down: "ArrowDown",
|
|
1984
|
+
left: "ArrowLeft",
|
|
1985
|
+
right: "ArrowRight",
|
|
1986
|
+
home: "Home",
|
|
1987
|
+
end: "End",
|
|
1988
|
+
page_up: "PageUp",
|
|
1989
|
+
page_down: "PageDown",
|
|
1990
|
+
})
|
|
1991
|
+
const KEY_FILTER_MODIFIERS = Object.freeze(["meta", "ctrl", "alt", "shift"])
|
|
1992
|
+
|
|
1993
|
+
// Stimulus's rule: the four modifiers must match EXACTLY (ctrl+k is not k);
|
|
1994
|
+
// the remaining token is compared to event.key case-insensitively. A
|
|
1995
|
+
// non-keyboard event (no event.key) checks the modifiers only. `keyMappings`
|
|
1996
|
+
// is the app's Stimulus schema table (custom keys included) when the
|
|
1997
|
+
// controller has one; the default table is the fallback.
|
|
1998
|
+
function keyFilterMatches(filter, event, keyMappings) {
|
|
1999
|
+
const parts = filter.split("+")
|
|
2000
|
+
for (const mod of KEY_FILTER_MODIFIERS) {
|
|
2001
|
+
if (parts.includes(mod) !== Boolean(event[`${mod}Key`])) return false
|
|
2002
|
+
}
|
|
2003
|
+
const token = parts.find((part) => !KEY_FILTER_MODIFIERS.includes(part))
|
|
2004
|
+
if (token === undefined || typeof event.key !== "string") return true
|
|
2005
|
+
const key =
|
|
2006
|
+
keyMappings && Object.hasOwn(keyMappings, token)
|
|
2007
|
+
? keyMappings[token]
|
|
2008
|
+
: Object.hasOwn(KEY_FILTER_MAP, token)
|
|
2009
|
+
? KEY_FILTER_MAP[token]
|
|
2010
|
+
: /^[a-z0-9]$/.test(token)
|
|
2011
|
+
? token
|
|
2012
|
+
: null
|
|
2013
|
+
return typeof key === "string" && key.toLowerCase() === event.key.toLowerCase()
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// Parse data-reactive-ops-param into binding records. Stimulus typecasts a
|
|
2017
|
+
// lone record to an object; several records stay a string (JSON.parse fails on
|
|
2018
|
+
// the join) and are split here. A LEGACY [[op, args]] list (array or JSON
|
|
2019
|
+
// string — a hand-built attr) becomes one record flagged `legacy`: it has no
|
|
2020
|
+
// `on`, so it always matches, and runOps reads its flags from event.params. A
|
|
2021
|
+
// malformed piece warns and is skipped; its siblings still run (default-deny).
|
|
2022
|
+
function parseBindingRecords(raw) {
|
|
2023
|
+
if (Array.isArray(raw)) return [{ ops: raw, legacy: true }]
|
|
2024
|
+
if (raw && typeof raw === "object") return [raw]
|
|
2025
|
+
if (typeof raw !== "string") return []
|
|
2026
|
+
const text = raw.trim()
|
|
2027
|
+
if (text.startsWith("[")) return [{ ops: parseOps(text), legacy: true }]
|
|
2028
|
+
const records = []
|
|
2029
|
+
for (const piece of text.split(" ")) {
|
|
2030
|
+
if (piece === "") continue
|
|
2031
|
+
let record = null
|
|
2032
|
+
try {
|
|
2033
|
+
record = JSON.parse(piece)
|
|
2034
|
+
} catch {
|
|
2035
|
+
record = null
|
|
2036
|
+
}
|
|
2037
|
+
if (record && typeof record === "object" && !Array.isArray(record)) records.push(record)
|
|
2038
|
+
else console.warn(`[phlex-reactive] malformed on_client binding record ${JSON.stringify(piece)} — skipped`)
|
|
2039
|
+
}
|
|
2040
|
+
return records
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// Does this record's descriptor match the firing event? The window check
|
|
2044
|
+
// matters for an element carrying both `click` and `click@window`: one inside
|
|
2045
|
+
// click reaches both listeners, and only currentTarget tells them apart. The
|
|
2046
|
+
// `window != null` guard keeps a window-less harness from classifying every
|
|
2047
|
+
// undefined currentTarget as window-bound. Only a LEGACY record (no `on` by
|
|
2048
|
+
// construction) matches everything; any other record without a descriptor is
|
|
2049
|
+
// malformed and never matches (default-deny). `keyMappings === null` skips the
|
|
2050
|
+
// key filter (the caller's single-candidate shortcut).
|
|
2051
|
+
function bindingMatches(record, event, keyMappings) {
|
|
2052
|
+
if (record.legacy) return true
|
|
2053
|
+
const on = record.on
|
|
2054
|
+
if (typeof on !== "string" || on === "") return false
|
|
2055
|
+
const dot = on.indexOf(".")
|
|
2056
|
+
if (event.type !== (dot < 0 ? on : on.slice(0, dot))) return false
|
|
2057
|
+
const win = globalThis.window
|
|
2058
|
+
const windowBound = win != null && event.currentTarget === win
|
|
2059
|
+
if (Boolean(record.window) !== windowBound) return false
|
|
2060
|
+
return dot < 0 || keyMappings === null || keyFilterMatches(on.slice(dot + 1), event, keyMappings)
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// A `once: true` record (PR #272) is spent after its first run. Stimulus
|
|
2064
|
+
// removes only that binding's own listener; a regular same-event sibling on
|
|
2065
|
+
// the element keeps calling runOps with BOTH records, so the spent one is
|
|
2066
|
+
// skipped here. Spent state belongs to the TRIGGER element (a WeakMap, so it
|
|
2067
|
+
// is collected with it): a morph/stream that swaps in a fresh element starts
|
|
2068
|
+
// fresh, and a byte-identical sibling trigger has its own. A window-bound
|
|
2069
|
+
// binding's currentTarget is the window, so there the element's ops attr joins
|
|
2070
|
+
// the key (a re-rendered window-bound once trigger with the same markup stays
|
|
2071
|
+
// spent — a known limit).
|
|
2072
|
+
const spentOnceBindings = new WeakMap()
|
|
2073
|
+
function onceBindingSpent(controller, event, record) {
|
|
2074
|
+
if (!record.once) return false
|
|
2075
|
+
const owner = event.currentTarget ?? controller.element
|
|
2076
|
+
let byOwner = spentOnceBindings.get(controller)
|
|
2077
|
+
if (!byOwner) {
|
|
2078
|
+
byOwner = new WeakMap()
|
|
2079
|
+
spentOnceBindings.set(controller, byOwner)
|
|
2080
|
+
}
|
|
2081
|
+
let spent = byOwner.get(owner)
|
|
2082
|
+
if (!spent) {
|
|
2083
|
+
spent = new Set()
|
|
2084
|
+
byOwner.set(owner, spent)
|
|
2085
|
+
}
|
|
2086
|
+
const raw = event.params?.ops
|
|
2087
|
+
const attr = owner === globalThis.window ? `${typeof raw === "string" ? raw : JSON.stringify(raw ?? null)}|` : ""
|
|
2088
|
+
const key = attr + JSON.stringify(record)
|
|
2089
|
+
if (spent.has(key)) return true
|
|
2090
|
+
spent.add(key)
|
|
2091
|
+
return false
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// Two identical descriptors on one element (two mix-ed on_client calls with
|
|
2095
|
+
// the same event) are two Stimulus bindings, so runOps runs twice for ONE
|
|
2096
|
+
// event. The first call runs every matching record; a repeat is a no-op.
|
|
2097
|
+
// "Repeat" is (controller, listener target, the element's ops attr): Stimulus
|
|
2098
|
+
// walks EVERY binding of one window listener with the SAME event object, so
|
|
2099
|
+
// keying on currentTarget alone (= window) would drop every other root's
|
|
2100
|
+
// window-bound binding — and a sibling element's within one root. The ops attr
|
|
2101
|
+
// is the per-element key (a lone record is typecast to a fresh object per
|
|
2102
|
+
// binding, so it is compared by its JSON). Two elements in one root carrying
|
|
2103
|
+
// byte-identical window-bound records run once — the same ops, applied once.
|
|
2104
|
+
const ranBindings = new WeakMap()
|
|
2105
|
+
function bindingsAlreadyRan(event, controller) {
|
|
2106
|
+
if (event === null || typeof event !== "object") return false
|
|
2107
|
+
const raw = event.params?.ops
|
|
2108
|
+
const key = typeof raw === "string" ? raw : JSON.stringify(raw ?? null)
|
|
2109
|
+
let byController = ranBindings.get(event)
|
|
2110
|
+
if (!byController) {
|
|
2111
|
+
byController = new WeakMap()
|
|
2112
|
+
ranBindings.set(event, byController)
|
|
2113
|
+
}
|
|
2114
|
+
let byTarget = byController.get(controller)
|
|
2115
|
+
if (!byTarget) {
|
|
2116
|
+
byTarget = new Map()
|
|
2117
|
+
byController.set(controller, byTarget)
|
|
2118
|
+
}
|
|
2119
|
+
let keys = byTarget.get(event.currentTarget)
|
|
2120
|
+
if (!keys) {
|
|
2121
|
+
keys = new Set()
|
|
2122
|
+
byTarget.set(event.currentTarget, keys)
|
|
2123
|
+
}
|
|
2124
|
+
if (keys.has(key)) return true
|
|
2125
|
+
keys.add(key)
|
|
2126
|
+
return false
|
|
2127
|
+
}
|
|
2128
|
+
|
|
1950
2129
|
// The TEXT reading of a compute control (issue #262) — what a :string input
|
|
1951
2130
|
// hands the reducer and what the identity/cross-root mirrors paint. A checkbox
|
|
1952
2131
|
// reads its CHECKED STATE ("true"/"false", the strings reactive_show compares
|
|
@@ -2067,7 +2246,7 @@ function applyOps(list, resolveTargets, onZeroTargets) {
|
|
|
2067
2246
|
}
|
|
2068
2247
|
const targets = resolveTargets(args)
|
|
2069
2248
|
if (targets.length === 0 && onZeroTargets) onZeroTargets(name, args)
|
|
2070
|
-
for (const el of targets) CLIENT_OPS[name](el, args)
|
|
2249
|
+
for (const el of targets) CLIENT_OPS[name](el, args, resolveTargets)
|
|
2071
2250
|
}
|
|
2072
2251
|
}
|
|
2073
2252
|
|
|
@@ -2606,11 +2785,33 @@ export default class extends Controller {
|
|
|
2606
2785
|
// the component resets whatever they toggled (by design — a signed action
|
|
2607
2786
|
// owns state that must survive re-renders).
|
|
2608
2787
|
runOps(event) {
|
|
2609
|
-
|
|
2788
|
+
if (bindingsAlreadyRan(event, this)) return
|
|
2789
|
+
const params = event.params ?? {}
|
|
2610
2790
|
// The trigger element on_client was spread onto (issue #222 ctx: { el }),
|
|
2611
2791
|
// captured now — currentTarget resets before the confirm resolver's microtask.
|
|
2612
2792
|
const trigger = event.currentTarget ?? event.target
|
|
2793
|
+
// Issue #271: run only the binding record(s) whose descriptor fired. A
|
|
2794
|
+
// legacy [[op, args]] attr reads its flags from the element-wide params.
|
|
2795
|
+
const records = parseBindingRecords(params.ops)
|
|
2796
|
+
// Stimulus already applied the firing descriptor's key filter (custom
|
|
2797
|
+
// schema keys included), so a lone candidate on type + window-boundness
|
|
2798
|
+
// runs without a second key check; several same-type candidates are told
|
|
2799
|
+
// apart by their key filters, read through the app's Stimulus schema.
|
|
2800
|
+
const candidates = records.filter((record) => bindingMatches(record, event, null))
|
|
2801
|
+
const matching =
|
|
2802
|
+
candidates.length <= 1
|
|
2803
|
+
? candidates
|
|
2804
|
+
: candidates.filter((record) => bindingMatches(record, event, this.application?.schema?.keyMappings))
|
|
2805
|
+
if (matching.length === 0 && records.length > 0) return this.#warnNoBinding(event)
|
|
2806
|
+
for (const record of matching) {
|
|
2807
|
+
if (onceBindingSpent(this, event, record)) continue
|
|
2808
|
+
this.#runBinding(record.legacy ? { ...params, ops: record.ops } : record, event, trigger)
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2613
2811
|
|
|
2812
|
+
// One on_client binding (issue #95): its outside guard, preventDefault rule,
|
|
2813
|
+
// confirm gate, then its ops — every flag read from the binding itself.
|
|
2814
|
+
#runBinding({ ops, confirm, confirmWhen, outside, window: windowBound }, event, trigger) {
|
|
2614
2815
|
// Outside guard FIRST — identical semantics to dispatch() (issue #80): an
|
|
2615
2816
|
// outside: trigger is a COMPLETE no-op for events inside this root, before
|
|
2616
2817
|
// preventDefault and before any op runs.
|
|
@@ -2648,6 +2849,17 @@ export default class extends Controller {
|
|
|
2648
2849
|
})
|
|
2649
2850
|
}
|
|
2650
2851
|
|
|
2852
|
+
// Issue #271: runOps fired but no record matched the event — a hand-edited
|
|
2853
|
+
// attr or a descriptor the matcher doesn't know. Verbose gate only, deduped.
|
|
2854
|
+
#warnNoBinding(event) {
|
|
2855
|
+
if (!this.#verboseEnabled()) return
|
|
2856
|
+
const key = `no-binding|${event.type}|${this.element?.id || "?"}`
|
|
2857
|
+
if (zeroTargetAlreadyWarned(key)) return
|
|
2858
|
+
console.warn(
|
|
2859
|
+
`[phlex-reactive] runOps on #${this.element?.id || "?"} found no on_client binding matching a "${event.type}" event — nothing ran`,
|
|
2860
|
+
)
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2651
2863
|
// Dirty tracking (issue #103). Wired by reactive_field(dirty: true) /
|
|
2652
2864
|
// reactive_root(track_dirty: true): an `input` on an owned field runs a FULL
|
|
2653
2865
|
// re-scan of every field this root owns. NO round trip, NO shipped state — the
|
|
@@ -2882,12 +3094,25 @@ export default class extends Controller {
|
|
|
2882
3094
|
// CLICKING IT (so its own on(:select) reactive trigger fires — selection stays
|
|
2883
3095
|
// a signed action); Escape clears. Ephemeral highlight state lives on the DOM
|
|
2884
3096
|
// (data-reactive-highlighted), never shipped to the client as trusted state.
|
|
3097
|
+
//
|
|
3098
|
+
// Issue #271: a container spread with reactive_listnav(focus: true) carries
|
|
3099
|
+
// data-reactive-listnav-focus-param="true" — ROVING-FOCUS mode for a
|
|
3100
|
+
// role=menu: the same moves shift real focus among the items instead of a
|
|
3101
|
+
// highlight, and Home/End (listnavFirst/listnavLast) jump to the edges.
|
|
2885
3102
|
listnavNext(event) {
|
|
2886
|
-
this.#
|
|
3103
|
+
this.#listnavMove(event, (current, length) => (current < 0 ? 0 : (current + 1) % length))
|
|
2887
3104
|
}
|
|
2888
3105
|
|
|
2889
3106
|
listnavPrev(event) {
|
|
2890
|
-
this.#
|
|
3107
|
+
this.#listnavMove(event, (current, length) => (current < 0 ? length - 1 : (current - 1 + length) % length))
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
listnavFirst(event) {
|
|
3111
|
+
this.#listnavMove(event, () => 0)
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
listnavLast(event) {
|
|
3115
|
+
this.#listnavMove(event, (_current, length) => length - 1)
|
|
2891
3116
|
}
|
|
2892
3117
|
|
|
2893
3118
|
// Enter: activate the highlighted option (fires its reactive select). No-op if
|
|
@@ -2905,23 +3130,39 @@ export default class extends Controller {
|
|
|
2905
3130
|
for (const el of this.#listnavOptions(event)) el.removeAttribute("data-reactive-highlighted")
|
|
2906
3131
|
}
|
|
2907
3132
|
|
|
2908
|
-
// Move
|
|
2909
|
-
//
|
|
2910
|
-
|
|
3133
|
+
// Move among THIS root's options: `pick(current, length)` returns the next
|
|
3134
|
+
// index (current is -1 when nothing is highlighted/focused). preventDefault
|
|
3135
|
+
// stops Arrow keys moving the caret in a search input, and the page scrolling
|
|
3136
|
+
// under a menu. Highlight mode writes data-reactive-highlighted; focus mode
|
|
3137
|
+
// (issue #271) focuses the chosen item, current being the item that is or
|
|
3138
|
+
// contains document.activeElement.
|
|
3139
|
+
#listnavMove(event, pick) {
|
|
2911
3140
|
const options = this.#listnavOptions(event)
|
|
2912
3141
|
if (!options.length) return
|
|
2913
3142
|
event.preventDefault()
|
|
2914
3143
|
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
const
|
|
3144
|
+
const focusMode = this.#listnavFocusMode(event)
|
|
3145
|
+
const active = globalThis.document?.activeElement
|
|
3146
|
+
const current = focusMode
|
|
3147
|
+
? options.findIndex((el) => el === active || (active != null && el.contains?.(active)))
|
|
3148
|
+
: options.findIndex((el) => el.hasAttribute("data-reactive-highlighted"))
|
|
3149
|
+
const chosen = options[pick(current, options.length)]
|
|
2918
3150
|
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3151
|
+
if (focusMode) {
|
|
3152
|
+
chosen.focus?.()
|
|
3153
|
+
} else {
|
|
3154
|
+
for (const el of options) el.removeAttribute("data-reactive-highlighted")
|
|
3155
|
+
chosen.setAttribute("data-reactive-highlighted", "true")
|
|
3156
|
+
}
|
|
2922
3157
|
chosen.scrollIntoView?.({ block: "nearest" })
|
|
2923
3158
|
}
|
|
2924
3159
|
|
|
3160
|
+
// Focus mode is read off the TRIGGER like the option selector (issue #271).
|
|
3161
|
+
#listnavFocusMode(event) {
|
|
3162
|
+
const trigger = event?.currentTarget ?? event?.target ?? this.element
|
|
3163
|
+
return trigger.getAttribute?.("data-reactive-listnav-focus-param") === "true"
|
|
3164
|
+
}
|
|
3165
|
+
|
|
2925
3166
|
// The option elements this root owns (skips nested reactive roots, issue #15),
|
|
2926
3167
|
// per the selector on data-reactive-listnav-option-param. The attr rides on the
|
|
2927
3168
|
// TRIGGER element (the search input on(...) is spread onto), read from the
|