phlex-reactive 0.16.7 → 0.16.8

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.
@@ -136,6 +136,20 @@ export function registerReactiveToken() {
136
136
  // is emitted AFTER all render streams in the reply (the endpoint appends it
137
137
  // last), so focus("[name=next]") sees the freshly morphed DOM — Turbo applies
138
138
  // streams in document order.
139
+ //
140
+ // Focus ops wait one frame (issue #446). Turbo renders a stream message inside
141
+ // its focus preservation: it reads document.activeElement, appends the
142
+ // streams, waits one animation frame, reads it again and, when the element
143
+ // focused before has an id and focus moved, focuses that element again. This
144
+ // action runs behind a frame each stream queued before that compare, so a
145
+ // focus, focus_first or blur applied here would be undone, and the old
146
+ // field's focus binding would run again. The ops before the first focus op
147
+ // still run now; that op and the rest of its chain (order kept) run in a frame
148
+ // queued from here, the one after the compare, which then saw nothing move and
149
+ // restored nothing. Plain requestAnimationFrame, never a timeout: in a hidden
150
+ // tab Turbo's compare waits for the first frame too.
151
+ const FOCUS_OPS = ["focus", "focus_first", "blur"]
152
+
139
153
  export function registerReactiveJs() {
140
154
  const actions = window.Turbo?.StreamActions
141
155
  if (!actions || actions["reactive:js"]) return
@@ -155,16 +169,21 @@ export function registerReactiveJs() {
155
169
  }
156
170
  return
157
171
  }
158
- applyOps(
159
- list,
160
- (args) => streamOpTargets(args, root),
161
- verbose
162
- ? (name, args) =>
163
- __SPLIT__
164
- ? withFeature("devtools", (devtools) => devtools.diagnoseStream(name, args, root))
165
- : devtoolsFeature.diagnoseStream(name, args, root)
166
- : undefined,
167
- )
172
+ const run = (ops) =>
173
+ applyOps(
174
+ ops,
175
+ (args) => streamOpTargets(args, root),
176
+ verbose
177
+ ? (name, args) =>
178
+ __SPLIT__
179
+ ? withFeature("devtools", (devtools) => devtools.diagnoseStream(name, args, root))
180
+ : devtoolsFeature.diagnoseStream(name, args, root)
181
+ : undefined,
182
+ )
183
+ const at = list.findIndex((entry) => FOCUS_OPS.includes(entry?.[0]))
184
+ if (at < 0 || typeof requestAnimationFrame !== "function") return run(list)
185
+ run(list.slice(0, at))
186
+ requestAnimationFrame(() => run(list.slice(at)))
168
187
  }
169
188
  }
170
189
 
@@ -2046,6 +2065,10 @@ function streamOpTargets(args, root) {
2046
2065
  // core.owns(el) is this control this root's own, not
2047
2066
  // a nested reactive root's (issue #15)
2048
2067
  // core.ownership() the hoisted per-pass form of owns
2068
+ // core.bindingOwns(el) may this root's bindings READ this
2069
+ // control: owns, or a shared field
2070
+ // just below (issue #440)
2071
+ // core.bindingOwnership() the hoisted per-pass form of it
2049
2072
  // core.opTargets(args) the elements an op's `to:` names
2050
2073
  // core.applyOps(list, defaultTo) run an op list root-scoped
2051
2074
  // core.diagnose(label, args) the zero-target warning
@@ -2991,6 +3014,8 @@ export default class extends Controller {
2991
3014
  },
2992
3015
  owns: (el) => this.#ownsField(el),
2993
3016
  ownership: () => this.#ownershipFilter(),
3017
+ bindingOwns: (el) => this.#bindingOwns(el),
3018
+ bindingOwnership: () => this.#bindingOwnership(),
2994
3019
  opTargets: (args) => this.#opTargets(args),
2995
3020
  diagnose: (label, args) => this.#diagnoseZeroTargets(label, args),
2996
3021
  // Apply an op list root-scoped, with the zero-target diagnostics; a
@@ -4397,7 +4422,8 @@ export default class extends Controller {
4397
4422
  // the control's own server root sits directly below this one. One level: a
4398
4423
  // grand-host never sees it. Whatever reads #collectFields sees it too: the
4399
4424
  // request, and the conditional confirm (it judges the fields the action is
4400
- // about to post). Bindings, computes, listnav, dirty tracking and persist
4425
+ // about to post). The client bindings read such a control too
4426
+ // (#bindingOwnership); listnav, ops, busy_on, dirty tracking and persist
4401
4427
  // keep the strict nearest-root #ownershipFilter / #ownsField.
4402
4428
  #serverFieldFilter() {
4403
4429
  const nested = this.element.querySelectorAll(AWAKE_ROOT)
@@ -4410,6 +4436,30 @@ export default class extends Controller {
4410
4436
  }
4411
4437
  }
4412
4438
 
4439
+ // What this root's client BINDINGS read (issue #440): its own controls, as
4440
+ // #ownsField, plus a control marked data-reactive-shared-field whose own
4441
+ // server root is this one (under client-only roots, issue #410) or sits
4442
+ // directly below it: one level, the #serverFieldFilter rule. Field reads
4443
+ // only: show (every term, after:, clear:, disable: true), enable, show and
4444
+ // group targets, checked_* counts, on_complete, media, css vars, compute
4445
+ // inputs and outputs. Which binding ELEMENTS a root evaluates, and every op, listnav,
4446
+ // busy_on, dirty and persist read, stay on the strict #ownsField. An
4447
+ // unmarked control in a nested root costs one attribute read more.
4448
+ #bindingOwns(el) {
4449
+ if (this.#ownsField(el)) return true
4450
+ if (el.getAttribute?.("data-reactive-shared-field") == null) return false
4451
+ const own = serverRoot(el, AWAKE_ROOT, this.element)
4452
+ return own === this.element || serverRoot(own?.parentElement, AWAKE_ROOT, this.element) === this.element
4453
+ }
4454
+
4455
+ // The hoisted per-pass form of #bindingOwns, with #ownershipFilter's fast
4456
+ // path: no nested root, every control is this root's. Never cached on the
4457
+ // instance (a morph replaces nodes).
4458
+ #bindingOwnership() {
4459
+ if (this.element.querySelectorAll(AWAKE_ROOT).length === 0) return () => true
4460
+ return (el) => this.#bindingOwns(el)
4461
+ }
4462
+
4413
4463
  // One walk over THIS root's named controls (not a nested reactive root's),
4414
4464
  // returning both the scalar `fields` and any chosen `files`. The ownership
4415
4465
  // predicate is hoisted ONCE (issue #117) via #ownershipFilter — in the common
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.16.7"
5
+ VERSION = "0.16.8"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.7
4
+ version: 0.16.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson