phlex-reactive 0.16.0 → 0.16.2

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +282 -145
  3. data/README.md +85 -1
  4. data/app/controllers/phlex/reactive/actions_controller.rb +10 -0
  5. data/app/javascript/phlex/reactive/core.js +1 -0
  6. data/app/javascript/phlex/reactive/core.min.js +4 -4
  7. data/app/javascript/phlex/reactive/core.min.js.map +4 -4
  8. data/app/javascript/phlex/reactive/features/bindings.js +81 -4
  9. data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
  10. data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
  11. data/app/javascript/phlex/reactive/features/drag.js +522 -0
  12. data/app/javascript/phlex/reactive/features/drag.min.js +4 -0
  13. data/app/javascript/phlex/reactive/features/drag.min.js.map +10 -0
  14. data/app/javascript/phlex/reactive/reactive_controller.js +2 -0
  15. data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
  16. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +7 -6
  17. data/app/javascript/phlex/reactive/runtime.js +57 -23
  18. data/lib/phlex/reactive/component/css_vars_binding.rb +118 -0
  19. data/lib/phlex/reactive/component/drag.rb +270 -0
  20. data/lib/phlex/reactive/component/dsl.rb +3 -0
  21. data/lib/phlex/reactive/component/helpers.rb +5 -1
  22. data/lib/phlex/reactive/component/registry.rb +15 -5
  23. data/lib/phlex/reactive/engine.rb +14 -5
  24. data/lib/phlex/reactive/held_fields.rb +21 -1
  25. data/lib/phlex/reactive/param_schema.rb +5 -1
  26. data/lib/phlex/reactive/response.rb +55 -4
  27. data/lib/phlex/reactive/shareable.rb +18 -3
  28. data/lib/phlex/reactive/test_helpers.rb +6 -0
  29. data/lib/phlex/reactive/version.rb +1 -1
  30. data/sig/phlex/reactive/response.rbs +3 -0
  31. metadata +6 -1
@@ -0,0 +1,522 @@
1
+ // phlex/reactive/features/drag — move and resize a box, or move a point,
2
+ // inside a frame, with the result in fields as fractions of the frame (issue
3
+ // #416). One of the feature modules the core imports on demand (issue #275);
4
+ // in the default client it is part of the one file.
5
+ //
6
+ // The markup is the Ruby helpers' (Phlex::Reactive::Component::Drag):
7
+ //
8
+ // data-reactive-frame the coordinate space: its padding box
9
+ // data-reactive-box='{"x":"crop[x]",…}' a box and the names of its fields
10
+ // -min / -ratio / -aspect / -action / -busy / -messages
11
+ // data-reactive-box-handle="se" an edge or corner (inside the box)
12
+ // data-reactive-point='{"x":…,"y":…}' a point (its centre)
13
+ // -within / -action / -busy / -messages
14
+ //
15
+ // and what it sets while a pointer drag is on, for the app's CSS:
16
+ //
17
+ // data-reactive-frame-active on the frame
18
+ // data-reactive-dragging on the pressed box, handle or point
19
+ //
20
+ // Where an element is lives in its own inline style — left/top/width/height
21
+ // in percent, rendered by the server and written here — so a morph that
22
+ // renders new values moves it, and nothing is cached between drags. Each
23
+ // field's .value is the same number as a fraction, rounded to 4 decimals and
24
+ // written as set_value writes (core.setValue: a bubbling `input` when it
25
+ // moved), at most once a frame while a pointer drags; one `change` follows on
26
+ // release. A box's x/y/width/height are fractions of the frame; a point's x/y
27
+ // of the element it sits in that matches its -within selector, else of the
28
+ // frame.
29
+ //
30
+ // Input is pointer events: mouse, pen and touch behave the same (the helpers
31
+ // render touch-action: none, so a finger drag does not scroll), and a drag
32
+ // starts after a 4 px move. The keyboard moves the focused box or point
33
+ // directly — arrows 1%, Shift 10%, Alt resizes — and the key's release is
34
+ // the release. A declared action is sent once per release through the
35
+ // root's own pipeline (core.proceed).
36
+ //
37
+ // This module never imports the core: it reaches a controller through the
38
+ // `core` handle (core.owns, core.proceed, core.setValue).
39
+
40
+ const FRAME = "[data-reactive-frame]"
41
+ const BOX = "[data-reactive-box]"
42
+ const POINT = "[data-reactive-point]"
43
+ const HANDLE = "[data-reactive-box-handle]"
44
+ const GRIP = `${HANDLE},${POINT},${BOX}`
45
+ const ROOT = '[data-controller~="reactive"]'
46
+ const FIELD = "input,textarea,select,[contenteditable]"
47
+ const DRAGGING = "data-reactive-dragging"
48
+ const ACTIVE = "data-reactive-frame-active"
49
+ const THRESHOLD = 4 // px a press must move before it is a drag
50
+ const EDGE = 40 // px from the window's top or bottom where auto-scroll starts
51
+ const SPEED = 12 // px auto-scroll moves per frame
52
+ const ARROWS = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }
53
+ const STYLE = { x: "left", y: "top", width: "width", height: "height" }
54
+
55
+ // controller -> the removals of what connect() added.
56
+ const wired = new WeakMap()
57
+ // reactive root element -> its core handle, for a morph of a box in it.
58
+ const cores = new WeakMap()
59
+ // The one drag on the page, by pointer or keyboard, or null.
60
+ let drag = null
61
+ // The page's live region (re-created after a Turbo visit replaced <body>),
62
+ // and the frame that will write its next words.
63
+ let live
64
+ let speaking
65
+ let morphs = false
66
+
67
+ export function connect(controller, core) {
68
+ const root = controller.element
69
+ cores.set(root, core)
70
+ // Empty on the page before anything is announced (a screen reader often
71
+ // ignores a region that arrives already holding words).
72
+ liveRegion()
73
+ holdMorphs()
74
+ const on = [
75
+ ["pointerdown", (event) => pointerDown(event, core, root)],
76
+ ["keydown", (event) => keyDown(event, core, root)],
77
+ ["keyup", (event) => drag?.keyboard && event.target === drag.grip && ARROWS[event.key] && release()],
78
+ ["focusout", (event) => drag?.keyboard && event.target === drag.grip && release()],
79
+ ["input", (event) => aspectChanged(event, core, root)],
80
+ ["change", (event) => aspectChanged(event, core, root)],
81
+ ]
82
+ for (const [type, fn] of on) root.addEventListener(type, fn)
83
+ wired.set(controller, () => {
84
+ for (const [type, fn] of on) root.removeEventListener(type, fn)
85
+ })
86
+ }
87
+
88
+ export function disconnect(controller) {
89
+ wired.get(controller)?.()
90
+ wired.delete(controller)
91
+ cores.delete(controller.element)
92
+ // A root that leaves mid-drag (a replace, a navigation) ends its drag.
93
+ if (drag?.root === controller.element) settle(drag)
94
+ }
95
+
96
+ // --- Geometry: fractions, 0..1 on each axis --------------------------------------
97
+ const clamp = (value, low, high) => Math.min(Math.max(value, low), high)
98
+
99
+ export const round = (value) => Math.round(value * 1e4) / 1e4
100
+
101
+ // A position as the fields will hold it: each number rounded to 4 decimals,
102
+ // a box's x/y then pulled in so it still ends inside the frame (rounded
103
+ // apart, 0.45545 + 0.54455 is 0.4555 + 0.5446 = 1.0001).
104
+ export function snap(value) {
105
+ const out = { x: round(value.x), y: round(value.y) }
106
+ if (value.width != null) {
107
+ out.width = round(value.width)
108
+ out.height = round(value.height)
109
+ out.x = Math.min(out.x, round(1 - out.width))
110
+ out.y = Math.min(out.y, round(1 - out.height))
111
+ }
112
+ return out
113
+ }
114
+
115
+ export function moveBox(b, dx, dy) {
116
+ return { x: clamp(b.x + dx, 0, 1 - b.width), y: clamp(b.y + dy, 0, 1 - b.height), width: b.width, height: b.height }
117
+ }
118
+
119
+ export function movePoint(p, dx, dy) {
120
+ return { x: clamp(p.x + dx, 0, 1), y: clamp(p.y + dy, 0, 1) }
121
+ }
122
+
123
+ // Resize from `edge` (a handle: n, se, …) by (dx, dy), the opposite side
124
+ // staying put, inside the frame and never below `min`. `ratio` is the locked
125
+ // width over height IN FRACTIONS (0: free): on a corner the axis the pointer
126
+ // moved more drives, on an edge the other axis grows around the box's centre.
127
+ export function resizeBox(b, edge, dx, dy, min, ratio) {
128
+ const [n, e, s, w] = ["n", "e", "s", "w"].map((side) => edge.includes(side))
129
+ const right = b.x + b.width
130
+ const bottom = b.y + b.height
131
+ const cx = b.x + b.width / 2
132
+ const cy = b.y + b.height / 2
133
+ // From the side that stays to the frame's edge (an axis no handle drives,
134
+ // with a ratio: around its centre).
135
+ const roomW = e ? 1 - b.x : w ? right : ratio ? 2 * Math.min(cx, 1 - cx) : 1
136
+ const roomH = s ? 1 - b.y : n ? bottom : ratio ? 2 * Math.min(cy, 1 - cy) : 1
137
+ let width = b.width + (e ? dx : w ? -dx : 0)
138
+ let height = b.height + (s ? dy : n ? -dy : 0)
139
+ if (ratio) {
140
+ const byWidth = (e || w) && (!(n || s) || Math.abs(width - b.width) >= Math.abs(height - b.height) * ratio)
141
+ const high = Math.min(roomW, roomH * ratio)
142
+ width = clamp(byWidth ? width : height * ratio, Math.min(Math.max(min, min * ratio), high), high)
143
+ height = width / ratio
144
+ } else {
145
+ if (e || w) width = clamp(width, Math.min(min, roomW), roomW)
146
+ if (n || s) height = clamp(height, Math.min(min, roomH), roomH)
147
+ }
148
+ return { x: w ? right - width : e ? b.x : cx - width / 2, y: n ? bottom - height : s ? b.y : cy - height / 2, width, height }
149
+ }
150
+
151
+ // The box at a new `ratio` (fractions): the same area, centred where it was,
152
+ // inside the frame and no smaller than `min`.
153
+ export function fitBox(b, ratio, min) {
154
+ const high = Math.min(1, ratio)
155
+ const width = clamp(Math.sqrt(b.width * b.height * ratio), Math.min(Math.max(min, min * ratio), high), high)
156
+ const height = width / ratio
157
+ return {
158
+ x: clamp(b.x + b.width / 2 - width / 2, 0, 1 - width),
159
+ y: clamp(b.y + b.height / 2 - height / 2, 0, 1 - height),
160
+ width,
161
+ height,
162
+ }
163
+ }
164
+
165
+ // "16:9", "1.5" or "" (free): width over height, or 0 for free.
166
+ export function parseRatio(text) {
167
+ const [width, height = 1, extra] = String(text ?? "").split(":").map(Number)
168
+ const ratio = extra === undefined ? width / height : 0
169
+ return ratio > 0 && Number.isFinite(ratio) ? ratio : 0
170
+ }
171
+
172
+ // --- Reading and writing an element ------------------------------------------------
173
+ const attr = (el, name) => el?.getAttribute?.(`data-reactive-${name}`)
174
+ const json = (value, fallback) => {
175
+ try {
176
+ return value == null ? fallback : JSON.parse(value)
177
+ } catch {
178
+ return fallback
179
+ }
180
+ }
181
+ const kind = (el) => (el.matches(POINT) ? "point" : "box")
182
+ const keysOf = (el) => (kind(el) === "point" ? ["x", "y"] : ["x", "y", "width", "height"])
183
+
184
+ function read(el) {
185
+ const value = {}
186
+ for (const key of keysOf(el)) value[key] = (parseFloat(el.style[STYLE[key]]) || 0) / 100
187
+ return value
188
+ }
189
+
190
+ function write(el, value) {
191
+ for (const key of keysOf(el)) el.style[STYLE[key]] = `${Math.round(value[key] * 1e4) / 100}%`
192
+ }
193
+
194
+ // An element's padding box in viewport px: what CSS % on a child resolves against.
195
+ function paddingBox(el) {
196
+ const rect = el.getBoundingClientRect()
197
+ return {
198
+ left: rect.left + (el.clientLeft || 0),
199
+ top: rect.top + (el.clientTop || 0),
200
+ width: el.clientWidth || rect.width,
201
+ height: el.clientHeight || rect.height,
202
+ }
203
+ }
204
+
205
+ // A point's bounds: the element it sits in that matches -within, else its frame.
206
+ function boundsOf(el, frame) {
207
+ try {
208
+ return el.parentElement.closest(attr(el, "point-within") || FRAME) ?? frame
209
+ } catch {
210
+ return frame // an invalid selector
211
+ }
212
+ }
213
+
214
+ // The root's own fields of these names (a nested root's are its own, issue #15).
215
+ function named(root, core, name) {
216
+ return [...root.querySelectorAll("[name]")].filter((field) => field.name === name && core.owns(field))
217
+ }
218
+
219
+ function fieldsOf(el, root, core) {
220
+ const names = json(attr(el, kind(el)), {})
221
+ const fields = {}
222
+ for (const key of keysOf(el)) {
223
+ const [field] = named(root, core, names[key])
224
+ if (field) fields[key] = field
225
+ }
226
+ return fields
227
+ }
228
+
229
+ // The locked ratio in fractions (the pixel ratio over the frame's), or 0.
230
+ function ratioOf(box, frame, root, core) {
231
+ const name = attr(box, "box-aspect")
232
+ const field = name && named(root, core, name).find((f) => !/^(radio|checkbox)$/.test(f.type) || f.checked)
233
+ const ratio = name ? parseRatio(field?.value) : Number(attr(box, "box-ratio")) || 0
234
+ const { width, height } = paddingBox(frame)
235
+ return ratio && width && height ? (ratio * height) / width : 0
236
+ }
237
+
238
+ // What a drag (or a key, or a re-fit) works on, read off the element now.
239
+ function begin(core, root, grip, keyboard) {
240
+ const el = grip.matches(HANDLE) ? grip.closest(BOX) : grip
241
+ const frame = el?.closest(FRAME)
242
+ if (!frame) return null
243
+ const value = read(el)
244
+ const fields = fieldsOf(el, root, core)
245
+ const point = kind(el) === "point"
246
+ return {
247
+ core,
248
+ root,
249
+ grip,
250
+ el,
251
+ frame,
252
+ keyboard,
253
+ bounds: point ? boundsOf(el, frame) : frame,
254
+ edge: attr(grip, "box-handle"),
255
+ start: value,
256
+ value,
257
+ ratio: point ? 0 : ratioOf(el, frame, root, core),
258
+ min: Number(attr(el, "box-min")) || 0,
259
+ fields,
260
+ before: Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.value])),
261
+ }
262
+ }
263
+
264
+ // Write the fields now (set_value's write: `input` on each that moved).
265
+ function flush(record) {
266
+ cancelAnimationFrame(record.writing)
267
+ record.writing = 0
268
+ for (const [key, field] of Object.entries(record.fields)) record.core.setValue(field, String(round(record.value[key])))
269
+ }
270
+
271
+ // --- Pointer --------------------------------------------------------------------------
272
+ function pointerDown(event, core, root) {
273
+ forgetRemoved()
274
+ if (drag || event.button > 0 || event.isPrimary === false || event.target.closest?.(FIELD)) return
275
+ const grip = event.target.closest?.(GRIP)
276
+ const record = grip && core.owns(grip) && begin(core, root, grip, false)
277
+ if (!record) return
278
+ const x0 = event.clientX
279
+ const y0 = event.clientY
280
+ const bounds = paddingBox(record.bounds)
281
+ record.p0 = [(x0 - bounds.left) / bounds.width, (y0 - bounds.top) / bounds.height]
282
+ // Only the pointer that pressed drives the drag: a second finger is not it.
283
+ const mine = (e) => e.pointerId === event.pointerId
284
+ const move = (e) => {
285
+ // (A drag that ended before the release — its element left — stays over.)
286
+ if (!mine(e) || record.over) return
287
+ if (drag) return follow(e.clientX, e.clientY)
288
+ if (Math.abs(e.clientX - x0) <= THRESHOLD && Math.abs(e.clientY - y0) <= THRESHOLD) return
289
+ start(record)
290
+ follow(e.clientX, e.clientY)
291
+ }
292
+ const up = (e) => {
293
+ if (!mine(e)) return
294
+ end()
295
+ if (drag) suppressClick(), release()
296
+ }
297
+ const abort = (e) => {
298
+ if (e.type === "pointercancel" && !mine(e)) return
299
+ end()
300
+ if (drag) cancel()
301
+ }
302
+ const escape = (e) => e.key === "Escape" && drag && (e.preventDefault(), abort(e))
303
+ const on = [
304
+ ["pointermove", move],
305
+ ["pointerup", up],
306
+ ["pointercancel", abort],
307
+ ["keydown", escape],
308
+ ]
309
+ for (const [type, fn] of on) document.addEventListener(type, fn)
310
+ function end() {
311
+ for (const [type, fn] of on) document.removeEventListener(type, fn)
312
+ }
313
+ }
314
+
315
+ // The click a pointer drag ends with is not one.
316
+ function suppressClick() {
317
+ const stop = (event) => (event.preventDefault(), event.stopPropagation())
318
+ window.addEventListener("click", stop, { capture: true, once: true })
319
+ setTimeout(() => window.removeEventListener("click", stop, { capture: true }))
320
+ }
321
+
322
+ function start(record) {
323
+ drag = record
324
+ record.grip.setAttribute(DRAGGING, "")
325
+ record.frame.setAttribute(ACTIVE, "")
326
+ record.select = document.documentElement.style.userSelect
327
+ document.documentElement.style.userSelect = "none"
328
+ }
329
+
330
+ // The pointer is at (x, y): the element goes where the pointer's move since
331
+ // the press puts it, measured in its bounds as they are NOW (a scroll moved them).
332
+ function follow(x, y) {
333
+ const record = drag
334
+ // A morph or a stream took the element away: the drag is over.
335
+ if (!record.el.isConnected) return settle(record)
336
+ record.x = x
337
+ record.y = y
338
+ const bounds = paddingBox(record.bounds)
339
+ const dx = (x - bounds.left) / bounds.width - record.p0[0]
340
+ const dy = (y - bounds.top) / bounds.height - record.p0[1]
341
+ const { start: from, edge } = record
342
+ record.value = snap(
343
+ kind(record.el) === "point"
344
+ ? movePoint(from, dx, dy)
345
+ : edge
346
+ ? resizeBox(from, edge, dx, dy, record.min, record.ratio)
347
+ : moveBox(from, dx, dy),
348
+ )
349
+ write(record.el, record.value)
350
+ record.writing ||= requestAnimationFrame(() => flush(record))
351
+ scroll(y)
352
+ }
353
+
354
+ // Near the window's top or bottom edge, scroll one step a frame until the
355
+ // pointer leaves the edge (a frame taller than the window).
356
+ function scroll(y) {
357
+ drag.wy = y < EDGE ? -SPEED : y > window.innerHeight - EDGE ? SPEED : 0
358
+ if (drag.wy && !drag.raf) {
359
+ const step = () => {
360
+ if (!drag?.wy) return drag && (drag.raf = 0)
361
+ window.scrollBy(0, drag.wy)
362
+ follow(drag.x, drag.y)
363
+ if (drag) drag.raf = requestAnimationFrame(step)
364
+ }
365
+ drag.raf = requestAnimationFrame(step)
366
+ }
367
+ }
368
+
369
+ // A drag whose element left the page (a stream replaced it while an arrow
370
+ // was held: no keyup or focusout reaches it) is over, not in the way.
371
+ function forgetRemoved() {
372
+ if (drag && !drag.el.isConnected) settle(drag)
373
+ }
374
+
375
+ // --- Keyboard --------------------------------------------------------------------------
376
+ // A focused box, handle or point: the arrows move it (Shift: ten times as
377
+ // far), Alt+arrows resize a box from the focused handle or the bottom-right
378
+ // corner, Escape puts it back. The key's release ends the step (`change`, the
379
+ // action); each step is announced in the element's own words.
380
+ function keyDown(event, core, root) {
381
+ forgetRemoved()
382
+ const grip = event.target
383
+ // (A pointer drag, or a key held on another element, owns the drag.)
384
+ if (!grip?.matches?.(GRIP) || !core.owns(grip) || (drag && (!drag.keyboard || drag.grip !== grip))) return
385
+ if (event.key === "Escape") return drag && (event.preventDefault(), cancel())
386
+ const arrow = ARROWS[event.key]
387
+ if (!arrow) return
388
+ event.preventDefault()
389
+ drag ??= begin(core, root, grip, true)
390
+ if (!drag) return
391
+ const [dx, dy] = arrow.map((d) => d * (event.shiftKey ? 0.1 : 0.01))
392
+ const resize = event.altKey && kind(drag.el) === "box"
393
+ const now = drag.value
394
+ drag.value = snap(
395
+ kind(drag.el) === "point"
396
+ ? movePoint(now, dx, dy)
397
+ : resize
398
+ ? resizeBox(now, drag.edge ?? "se", dx, dy, drag.min, drag.ratio)
399
+ : moveBox(now, dx, dy),
400
+ )
401
+ write(drag.el, drag.value)
402
+ flush(drag)
403
+ announce(drag, resize ? "resized" : "moved")
404
+ }
405
+
406
+ function liveRegion() {
407
+ if (!live?.isConnected) {
408
+ live = document.createElement("div")
409
+ live.setAttribute("aria-live", "polite")
410
+ live.setAttribute("data-reactive-drag-live", "")
411
+ live.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap"
412
+ document.body.append(live)
413
+ }
414
+ return live
415
+ }
416
+
417
+ // Clear the region now and write the words on the next frame, so a change is
418
+ // heard even when the words are the same as the last ones.
419
+ function announce(record, name) {
420
+ let words = json(attr(record.el, `${kind(record.el)}-messages`), {})[name] ?? ""
421
+ for (const key of keysOf(record.el)) words = words.replaceAll(`{${key}}`, Math.round(record.value[key] * 100))
422
+ const region = liveRegion()
423
+ region.textContent = ""
424
+ cancelAnimationFrame(speaking)
425
+ speaking = requestAnimationFrame(() => (region.textContent = words))
426
+ }
427
+
428
+ // --- Aspect ---------------------------------------------------------------------------
429
+ // The field a box reads its ratio from changed (a preset radio was picked):
430
+ // the box re-fits to it around its centre, inside the frame — a committed
431
+ // change, like a release. A box already at that ratio stays as it is.
432
+ function aspectChanged(event, core, root) {
433
+ const name = event.target?.name
434
+ if (!name) return
435
+ for (const box of root.querySelectorAll("[data-reactive-box-aspect]")) {
436
+ if (attr(box, "box-aspect") !== name || box === drag?.el || !core.owns(box)) continue
437
+ const record = begin(core, root, box, true)
438
+ const { ratio, start: b } = record ?? {}
439
+ if (!ratio || Math.abs(b.width / b.height / ratio - 1) < 0.005) continue
440
+ record.value = snap(fitBox(b, ratio, record.min))
441
+ write(box, record.value)
442
+ flush(record)
443
+ commit(record)
444
+ }
445
+ }
446
+
447
+ // --- Release / cancel ---------------------------------------------------------------
448
+ function release() {
449
+ const record = drag
450
+ // A morph took the element away with no move since: what it rendered stands.
451
+ if (!record.el.isConnected) return settle(record)
452
+ flush(record)
453
+ settle(record)
454
+ commit(record)
455
+ }
456
+
457
+ // One `change` on each field the drag moved, and the action, once, when the
458
+ // element ended somewhere new.
459
+ function commit(record) {
460
+ for (const [key, field] of Object.entries(record.fields)) {
461
+ if (field.value !== record.before[key]) field.dispatchEvent(new Event("change", { bubbles: true }))
462
+ }
463
+ const name = kind(record.el)
464
+ const action = attr(record.el, `${name}-action`)
465
+ const params = {}
466
+ for (const key of keysOf(record.el)) params[key] = round(record.value[key])
467
+ if (!action || keysOf(record.el).every((key) => params[key] === round(record.start[key]))) return
468
+ record.core.proceed(record.el, action, JSON.stringify(params), json(attr(record.el, `${name}-busy`)))
469
+ }
470
+
471
+ function cancel() {
472
+ const record = drag
473
+ record.value = record.start
474
+ write(record.el, record.start)
475
+ flush(record)
476
+ if (record.keyboard) announce(record, "cancelled")
477
+ settle(record)
478
+ }
479
+
480
+ // Undo everything the drag set: the markers, the frame loops, the text selection.
481
+ function settle(record) {
482
+ drag = null
483
+ record.over = true
484
+ cancelAnimationFrame(record.raf)
485
+ cancelAnimationFrame(record.writing)
486
+ record.grip.removeAttribute(DRAGGING)
487
+ record.frame.removeAttribute(ACTIVE)
488
+ if (!record.keyboard) document.documentElement.style.userSelect = record.select ?? ""
489
+ }
490
+
491
+ // --- Morphs ----------------------------------------------------------------------------
492
+ // A morph that arrives mid-drag (a reply, a broadcast) does not move the
493
+ // dragged element, write its fields or drop the markers: the drag wins until
494
+ // release. A morph that moved a box or point outside a drag brings its fields
495
+ // along — they may sit outside what the morph rendered.
496
+ function holdMorphs() {
497
+ if (morphs) return
498
+ morphs = true
499
+ document.addEventListener("turbo:before-morph-attribute", (event) => {
500
+ const el = event.target
501
+ const name = event.detail?.attributeName
502
+ if (!drag) return
503
+ const held =
504
+ (el === drag.el && name === "style") ||
505
+ (el === drag.grip && name === DRAGGING) ||
506
+ (el === drag.frame && name === ACTIVE) ||
507
+ (name === "value" && Object.values(drag.fields).includes(el))
508
+ if (held) event.preventDefault()
509
+ })
510
+ // (Every morphed element fires this: the cheap check first.)
511
+ document.addEventListener("turbo:morph-element", (event) => {
512
+ const el = event.target
513
+ if (el === drag?.el || !el.matches?.(`${BOX},${POINT}`)) return
514
+ const root = el.closest(ROOT)
515
+ const core = cores.get(root)
516
+ if (!core?.owns(el)) return
517
+ const value = read(el)
518
+ for (const [key, field] of Object.entries(fieldsOf(el, root, core))) {
519
+ if (round(Number(field.value)) !== round(value[key])) core.setValue(field, String(round(value[key])))
520
+ }
521
+ })
522
+ }
@@ -0,0 +1,4 @@
1
+ var j={ArrowLeft:[-1,0],ArrowRight:[1,0],ArrowUp:[0,-1],ArrowDown:[0,1]},q={x:"left",y:"top",width:"width",height:"height"},I=new WeakMap,G=new WeakMap,i=null,y,H,B=!1;function ft(t,e){let n=t.element;G.set(n,e),Q(),lt();let c=[["pointerdown",(o)=>st(o,e,n)],["keydown",(o)=>ut(o,e,n)],["keyup",(o)=>i?.keyboard&&o.target===i.grip&&j[o.key]&&N()],["focusout",(o)=>i?.keyboard&&o.target===i.grip&&N()],["input",(o)=>X(o,e,n)],["change",(o)=>X(o,e,n)]];for(let[o,s]of c)n.addEventListener(o,s);I.set(t,()=>{for(let[o,s]of c)n.removeEventListener(o,s)})}function ht(t){if(I.get(t)?.(),I.delete(t),G.delete(t.element),i?.root===t.element)A(i)}var m=(t,e,n)=>Math.min(Math.max(t,e),n),d=(t)=>Math.round(t*1e4)/1e4;function S(t){let e={x:d(t.x),y:d(t.y)};if(t.width!=null)e.width=d(t.width),e.height=d(t.height),e.x=Math.min(e.x,d(1-e.width)),e.y=Math.min(e.y,d(1-e.height));return e}function V(t,e,n){return{x:m(t.x+e,0,1-t.width),y:m(t.y+n,0,1-t.height),width:t.width,height:t.height}}function W(t,e,n){return{x:m(t.x+e,0,1),y:m(t.y+n,0,1)}}function Y(t,e,n,c,o,s){let[r,a,l,f]=["n","e","s","w"].map((R)=>e.includes(R)),x=t.x+t.width,k=t.y+t.height,O=t.x+t.width/2,E=t.y+t.height/2,v=a?1-t.x:f?x:s?2*Math.min(O,1-O):1,u=l?1-t.y:r?k:s?2*Math.min(E,1-E):1,h=t.width+(a?n:f?-n:0),g=t.height+(l?c:r?-c:0);if(s){let R=(a||f)&&(!(r||l)||Math.abs(h-t.width)>=Math.abs(g-t.height)*s),P=Math.min(v,u*s);h=m(R?h:g*s,Math.min(Math.max(o,o*s),P),P),g=h/s}else{if(a||f)h=m(h,Math.min(o,v),v);if(r||l)g=m(g,Math.min(o,u),u)}return{x:f?x-h:a?t.x:O-h/2,y:r?k-g:l?t.y:E-g/2,width:h,height:g}}function et(t,e,n){let c=Math.min(1,e),o=m(Math.sqrt(t.width*t.height*e),Math.min(Math.max(n,n*e),c),c),s=o/e;return{x:m(t.x+t.width/2-o/2,0,1-o),y:m(t.y+t.height/2-s/2,0,1-s),width:o,height:s}}function nt(t){let[e,n=1,c]=String(t??"").split(":").map(Number),o=c===void 0?e/n:0;return o>0&&Number.isFinite(o)?o:0}var p=(t,e)=>t?.getAttribute?.(`data-reactive-${e}`),T=(t,e)=>{try{return t==null?e:JSON.parse(t)}catch{return e}},w=(t)=>t.matches("[data-reactive-point]")?"point":"box",b=(t)=>w(t)==="point"?["x","y"]:["x","y","width","height"];function z(t){let e={};for(let n of b(t))e[n]=(parseFloat(t.style[q[n]])||0)/100;return e}function D(t,e){for(let n of b(t))t.style[q[n]]=`${Math.round(e[n]*1e4)/100}%`}function F(t){let e=t.getBoundingClientRect();return{left:e.left+(t.clientLeft||0),top:e.top+(t.clientTop||0),width:t.clientWidth||e.width,height:t.clientHeight||e.height}}function ot(t,e){try{return t.parentElement.closest(p(t,"point-within")||"[data-reactive-frame]")??e}catch{return e}}function J(t,e,n){return[...t.querySelectorAll("[name]")].filter((c)=>c.name===n&&e.owns(c))}function K(t,e,n){let c=T(p(t,w(t)),{}),o={};for(let s of b(t)){let[r]=J(e,n,c[s]);if(r)o[s]=r}return o}function it(t,e,n,c){let o=p(t,"box-aspect"),s=o&&J(n,c,o).find((f)=>!/^(radio|checkbox)$/.test(f.type)||f.checked),r=o?nt(s?.value):Number(p(t,"box-ratio"))||0,{width:a,height:l}=F(e);return r&&a&&l?r*l/a:0}function C(t,e,n,c){let o=n.matches("[data-reactive-box-handle]")?n.closest("[data-reactive-box]"):n,s=o?.closest("[data-reactive-frame]");if(!s)return null;let r=z(o),a=K(o,e,t),l=w(o)==="point";return{core:t,root:e,grip:n,el:o,frame:s,keyboard:c,bounds:l?ot(o,s):s,edge:p(n,"box-handle"),start:r,value:r,ratio:l?0:it(o,s,e,t),min:Number(p(o,"box-min"))||0,fields:a,before:Object.fromEntries(Object.entries(a).map(([f,x])=>[f,x.value]))}}function M(t){cancelAnimationFrame(t.writing),t.writing=0;for(let[e,n]of Object.entries(t.fields))t.core.setValue(n,String(d(t.value[e])))}function st(t,e,n){if(U(),i||t.button>0||t.isPrimary===!1||t.target.closest?.("input,textarea,select,[contenteditable]"))return;let c=t.target.closest?.("[data-reactive-box-handle],[data-reactive-point],[data-reactive-box]"),o=c&&e.owns(c)&&C(e,n,c,!1);if(!o)return;let{clientX:s,clientY:r}=t,a=F(o.bounds);o.p0=[(s-a.left)/a.width,(r-a.top)/a.height];let l=(u)=>u.pointerId===t.pointerId,f=(u)=>{if(!l(u)||o.over)return;if(i)return L(u.clientX,u.clientY);if(Math.abs(u.clientX-s)<=4&&Math.abs(u.clientY-r)<=4)return;rt(o),L(u.clientX,u.clientY)},x=(u)=>{if(!l(u))return;if(v(),i)ct(),N()},k=(u)=>{if(u.type==="pointercancel"&&!l(u))return;if(v(),i)tt()},E=[["pointermove",f],["pointerup",x],["pointercancel",k],["keydown",(u)=>u.key==="Escape"&&i&&(u.preventDefault(),k(u))]];for(let[u,h]of E)document.addEventListener(u,h);function v(){for(let[u,h]of E)document.removeEventListener(u,h)}}function ct(){let t=(e)=>(e.preventDefault(),e.stopPropagation());window.addEventListener("click",t,{capture:!0,once:!0}),setTimeout(()=>window.removeEventListener("click",t,{capture:!0}))}function rt(t){i=t,t.grip.setAttribute("data-reactive-dragging",""),t.frame.setAttribute("data-reactive-frame-active",""),t.select=document.documentElement.style.userSelect,document.documentElement.style.userSelect="none"}function L(t,e){let n=i;if(!n.el.isConnected)return A(n);n.x=t,n.y=e;let c=F(n.bounds),o=(t-c.left)/c.width-n.p0[0],s=(e-c.top)/c.height-n.p0[1],{start:r,edge:a}=n;n.value=S(w(n.el)==="point"?W(r,o,s):a?Y(r,a,o,s,n.min,n.ratio):V(r,o,s)),D(n.el,n.value),n.writing||=requestAnimationFrame(()=>M(n)),at(e)}function at(t){if(i.wy=t<40?-12:t>window.innerHeight-40?12:0,i.wy&&!i.raf){let e=()=>{if(!i?.wy)return i&&(i.raf=0);if(window.scrollBy(0,i.wy),L(i.x,i.y),i)i.raf=requestAnimationFrame(e)};i.raf=requestAnimationFrame(e)}}function U(){if(i&&!i.el.isConnected)A(i)}function ut(t,e,n){U();let c=t.target;if(!c?.matches?.("[data-reactive-box-handle],[data-reactive-point],[data-reactive-box]")||!e.owns(c)||i&&(!i.keyboard||i.grip!==c))return;if(t.key==="Escape")return i&&(t.preventDefault(),tt());let o=j[t.key];if(!o)return;if(t.preventDefault(),i??=C(e,n,c,!0),!i)return;let[s,r]=o.map((f)=>f*(t.shiftKey?0.1:0.01)),a=t.altKey&&w(i.el)==="box",l=i.value;i.value=S(w(i.el)==="point"?W(l,s,r):a?Y(l,i.edge??"se",s,r,i.min,i.ratio):V(l,s,r)),D(i.el,i.value),M(i),Z(i,a?"resized":"moved")}function Q(){if(!y?.isConnected)y=document.createElement("div"),y.setAttribute("aria-live","polite"),y.setAttribute("data-reactive-drag-live",""),y.style.cssText="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap",document.body.append(y);return y}function Z(t,e){let n=T(p(t.el,`${w(t.el)}-messages`),{})[e]??"";for(let o of b(t.el))n=n.replaceAll(`{${o}}`,Math.round(t.value[o]*100));let c=Q();c.textContent="",cancelAnimationFrame(H),H=requestAnimationFrame(()=>c.textContent=n)}function X(t,e,n){let c=t.target?.name;if(!c)return;for(let o of n.querySelectorAll("[data-reactive-box-aspect]")){if(p(o,"box-aspect")!==c||o===i?.el||!e.owns(o))continue;let s=C(e,n,o,!0),{ratio:r,start:a}=s??{};if(!r||Math.abs(a.width/a.height/r-1)<0.005)continue;s.value=S(et(a,r,s.min)),D(o,s.value),M(s),_(s)}}function N(){let t=i;if(!t.el.isConnected)return A(t);M(t),A(t),_(t)}function _(t){for(let[o,s]of Object.entries(t.fields))if(s.value!==t.before[o])s.dispatchEvent(new Event("change",{bubbles:!0}));let e=w(t.el),n=p(t.el,`${e}-action`),c={};for(let o of b(t.el))c[o]=d(t.value[o]);if(!n||b(t.el).every((o)=>c[o]===d(t.start[o])))return;t.core.proceed(t.el,n,JSON.stringify(c),T(p(t.el,`${e}-busy`)))}function tt(){let t=i;if(t.value=t.start,D(t.el,t.start),M(t),t.keyboard)Z(t,"cancelled");A(t)}function A(t){if(i=null,t.over=!0,cancelAnimationFrame(t.raf),cancelAnimationFrame(t.writing),t.grip.removeAttribute("data-reactive-dragging"),t.frame.removeAttribute("data-reactive-frame-active"),!t.keyboard)document.documentElement.style.userSelect=t.select??""}function lt(){if(B)return;B=!0,document.addEventListener("turbo:before-morph-attribute",(t)=>{let e=t.target,n=t.detail?.attributeName;if(!i)return;if(e===i.el&&n==="style"||e===i.grip&&n==="data-reactive-dragging"||e===i.frame&&n==="data-reactive-frame-active"||n==="value"&&Object.values(i.fields).includes(e))t.preventDefault()}),document.addEventListener("turbo:morph-element",(t)=>{let e=t.target;if(e===i?.el||!e.matches?.("[data-reactive-box],[data-reactive-point]"))return;let n=e.closest('[data-controller~="reactive"]'),c=G.get(n);if(!c?.owns(e))return;let o=z(e);for(let[s,r]of Object.entries(K(e,n,c)))if(d(Number(r.value))!==d(o[s]))c.setValue(r,String(d(o[s])))})}export{ft as connect,ht as disconnect,et as fitBox,V as moveBox,W as movePoint,nt as parseRatio,Y as resizeBox,d as round,S as snap};
2
+
3
+ //# debugId=94F6177CA2EF4E8764756E2164756E21
4
+ //# sourceMappingURL=drag.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["features/drag.js"],
4
+ "sourcesContent": [
5
+ "// phlex/reactive/features/drag — move and resize a box, or move a point,\n// inside a frame, with the result in fields as fractions of the frame (issue\n// #416). One of the feature modules the core imports on demand (issue #275);\n// in the default client it is part of the one file.\n//\n// The markup is the Ruby helpers' (Phlex::Reactive::Component::Drag):\n//\n// data-reactive-frame the coordinate space: its padding box\n// data-reactive-box='{\"x\":\"crop[x]\",…}' a box and the names of its fields\n// -min / -ratio / -aspect / -action / -busy / -messages\n// data-reactive-box-handle=\"se\" an edge or corner (inside the box)\n// data-reactive-point='{\"x\":…,\"y\":…}' a point (its centre)\n// -within / -action / -busy / -messages\n//\n// and what it sets while a pointer drag is on, for the app's CSS:\n//\n// data-reactive-frame-active on the frame\n// data-reactive-dragging on the pressed box, handle or point\n//\n// Where an element is lives in its own inline style — left/top/width/height\n// in percent, rendered by the server and written here — so a morph that\n// renders new values moves it, and nothing is cached between drags. Each\n// field's .value is the same number as a fraction, rounded to 4 decimals and\n// written as set_value writes (core.setValue: a bubbling `input` when it\n// moved), at most once a frame while a pointer drags; one `change` follows on\n// release. A box's x/y/width/height are fractions of the frame; a point's x/y\n// of the element it sits in that matches its -within selector, else of the\n// frame.\n//\n// Input is pointer events: mouse, pen and touch behave the same (the helpers\n// render touch-action: none, so a finger drag does not scroll), and a drag\n// starts after a 4 px move. The keyboard moves the focused box or point\n// directly — arrows 1%, Shift 10%, Alt resizes — and the key's release is\n// the release. A declared action is sent once per release through the\n// root's own pipeline (core.proceed).\n//\n// This module never imports the core: it reaches a controller through the\n// `core` handle (core.owns, core.proceed, core.setValue).\n\nconst FRAME = \"[data-reactive-frame]\"\nconst BOX = \"[data-reactive-box]\"\nconst POINT = \"[data-reactive-point]\"\nconst HANDLE = \"[data-reactive-box-handle]\"\nconst GRIP = `${HANDLE},${POINT},${BOX}`\nconst ROOT = '[data-controller~=\"reactive\"]'\nconst FIELD = \"input,textarea,select,[contenteditable]\"\nconst DRAGGING = \"data-reactive-dragging\"\nconst ACTIVE = \"data-reactive-frame-active\"\nconst THRESHOLD = 4 // px a press must move before it is a drag\nconst EDGE = 40 // px from the window's top or bottom where auto-scroll starts\nconst SPEED = 12 // px auto-scroll moves per frame\nconst ARROWS = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }\nconst STYLE = { x: \"left\", y: \"top\", width: \"width\", height: \"height\" }\n\n// controller -> the removals of what connect() added.\nconst wired = new WeakMap()\n// reactive root element -> its core handle, for a morph of a box in it.\nconst cores = new WeakMap()\n// The one drag on the page, by pointer or keyboard, or null.\nlet drag = null\n// The page's live region (re-created after a Turbo visit replaced <body>),\n// and the frame that will write its next words.\nlet live\nlet speaking\nlet morphs = false\n\nexport function connect(controller, core) {\n const root = controller.element\n cores.set(root, core)\n // Empty on the page before anything is announced (a screen reader often\n // ignores a region that arrives already holding words).\n liveRegion()\n holdMorphs()\n const on = [\n [\"pointerdown\", (event) => pointerDown(event, core, root)],\n [\"keydown\", (event) => keyDown(event, core, root)],\n [\"keyup\", (event) => drag?.keyboard && event.target === drag.grip && ARROWS[event.key] && release()],\n [\"focusout\", (event) => drag?.keyboard && event.target === drag.grip && release()],\n [\"input\", (event) => aspectChanged(event, core, root)],\n [\"change\", (event) => aspectChanged(event, core, root)],\n ]\n for (const [type, fn] of on) root.addEventListener(type, fn)\n wired.set(controller, () => {\n for (const [type, fn] of on) root.removeEventListener(type, fn)\n })\n}\n\nexport function disconnect(controller) {\n wired.get(controller)?.()\n wired.delete(controller)\n cores.delete(controller.element)\n // A root that leaves mid-drag (a replace, a navigation) ends its drag.\n if (drag?.root === controller.element) settle(drag)\n}\n\n// --- Geometry: fractions, 0..1 on each axis --------------------------------------\nconst clamp = (value, low, high) => Math.min(Math.max(value, low), high)\n\nexport const round = (value) => Math.round(value * 1e4) / 1e4\n\n// A position as the fields will hold it: each number rounded to 4 decimals,\n// a box's x/y then pulled in so it still ends inside the frame (rounded\n// apart, 0.45545 + 0.54455 is 0.4555 + 0.5446 = 1.0001).\nexport function snap(value) {\n const out = { x: round(value.x), y: round(value.y) }\n if (value.width != null) {\n out.width = round(value.width)\n out.height = round(value.height)\n out.x = Math.min(out.x, round(1 - out.width))\n out.y = Math.min(out.y, round(1 - out.height))\n }\n return out\n}\n\nexport function moveBox(b, dx, dy) {\n return { x: clamp(b.x + dx, 0, 1 - b.width), y: clamp(b.y + dy, 0, 1 - b.height), width: b.width, height: b.height }\n}\n\nexport function movePoint(p, dx, dy) {\n return { x: clamp(p.x + dx, 0, 1), y: clamp(p.y + dy, 0, 1) }\n}\n\n// Resize from `edge` (a handle: n, se, …) by (dx, dy), the opposite side\n// staying put, inside the frame and never below `min`. `ratio` is the locked\n// width over height IN FRACTIONS (0: free): on a corner the axis the pointer\n// moved more drives, on an edge the other axis grows around the box's centre.\nexport function resizeBox(b, edge, dx, dy, min, ratio) {\n const [n, e, s, w] = [\"n\", \"e\", \"s\", \"w\"].map((side) => edge.includes(side))\n const right = b.x + b.width\n const bottom = b.y + b.height\n const cx = b.x + b.width / 2\n const cy = b.y + b.height / 2\n // From the side that stays to the frame's edge (an axis no handle drives,\n // with a ratio: around its centre).\n const roomW = e ? 1 - b.x : w ? right : ratio ? 2 * Math.min(cx, 1 - cx) : 1\n const roomH = s ? 1 - b.y : n ? bottom : ratio ? 2 * Math.min(cy, 1 - cy) : 1\n let width = b.width + (e ? dx : w ? -dx : 0)\n let height = b.height + (s ? dy : n ? -dy : 0)\n if (ratio) {\n const byWidth = (e || w) && (!(n || s) || Math.abs(width - b.width) >= Math.abs(height - b.height) * ratio)\n const high = Math.min(roomW, roomH * ratio)\n width = clamp(byWidth ? width : height * ratio, Math.min(Math.max(min, min * ratio), high), high)\n height = width / ratio\n } else {\n if (e || w) width = clamp(width, Math.min(min, roomW), roomW)\n if (n || s) height = clamp(height, Math.min(min, roomH), roomH)\n }\n return { x: w ? right - width : e ? b.x : cx - width / 2, y: n ? bottom - height : s ? b.y : cy - height / 2, width, height }\n}\n\n// The box at a new `ratio` (fractions): the same area, centred where it was,\n// inside the frame and no smaller than `min`.\nexport function fitBox(b, ratio, min) {\n const high = Math.min(1, ratio)\n const width = clamp(Math.sqrt(b.width * b.height * ratio), Math.min(Math.max(min, min * ratio), high), high)\n const height = width / ratio\n return {\n x: clamp(b.x + b.width / 2 - width / 2, 0, 1 - width),\n y: clamp(b.y + b.height / 2 - height / 2, 0, 1 - height),\n width,\n height,\n }\n}\n\n// \"16:9\", \"1.5\" or \"\" (free): width over height, or 0 for free.\nexport function parseRatio(text) {\n const [width, height = 1, extra] = String(text ?? \"\").split(\":\").map(Number)\n const ratio = extra === undefined ? width / height : 0\n return ratio > 0 && Number.isFinite(ratio) ? ratio : 0\n}\n\n// --- Reading and writing an element ------------------------------------------------\nconst attr = (el, name) => el?.getAttribute?.(`data-reactive-${name}`)\nconst json = (value, fallback) => {\n try {\n return value == null ? fallback : JSON.parse(value)\n } catch {\n return fallback\n }\n}\nconst kind = (el) => (el.matches(POINT) ? \"point\" : \"box\")\nconst keysOf = (el) => (kind(el) === \"point\" ? [\"x\", \"y\"] : [\"x\", \"y\", \"width\", \"height\"])\n\nfunction read(el) {\n const value = {}\n for (const key of keysOf(el)) value[key] = (parseFloat(el.style[STYLE[key]]) || 0) / 100\n return value\n}\n\nfunction write(el, value) {\n for (const key of keysOf(el)) el.style[STYLE[key]] = `${Math.round(value[key] * 1e4) / 100}%`\n}\n\n// An element's padding box in viewport px: what CSS % on a child resolves against.\nfunction paddingBox(el) {\n const rect = el.getBoundingClientRect()\n return {\n left: rect.left + (el.clientLeft || 0),\n top: rect.top + (el.clientTop || 0),\n width: el.clientWidth || rect.width,\n height: el.clientHeight || rect.height,\n }\n}\n\n// A point's bounds: the element it sits in that matches -within, else its frame.\nfunction boundsOf(el, frame) {\n try {\n return el.parentElement.closest(attr(el, \"point-within\") || FRAME) ?? frame\n } catch {\n return frame // an invalid selector\n }\n}\n\n// The root's own fields of these names (a nested root's are its own, issue #15).\nfunction named(root, core, name) {\n return [...root.querySelectorAll(\"[name]\")].filter((field) => field.name === name && core.owns(field))\n}\n\nfunction fieldsOf(el, root, core) {\n const names = json(attr(el, kind(el)), {})\n const fields = {}\n for (const key of keysOf(el)) {\n const [field] = named(root, core, names[key])\n if (field) fields[key] = field\n }\n return fields\n}\n\n// The locked ratio in fractions (the pixel ratio over the frame's), or 0.\nfunction ratioOf(box, frame, root, core) {\n const name = attr(box, \"box-aspect\")\n const field = name && named(root, core, name).find((f) => !/^(radio|checkbox)$/.test(f.type) || f.checked)\n const ratio = name ? parseRatio(field?.value) : Number(attr(box, \"box-ratio\")) || 0\n const { width, height } = paddingBox(frame)\n return ratio && width && height ? (ratio * height) / width : 0\n}\n\n// What a drag (or a key, or a re-fit) works on, read off the element now.\nfunction begin(core, root, grip, keyboard) {\n const el = grip.matches(HANDLE) ? grip.closest(BOX) : grip\n const frame = el?.closest(FRAME)\n if (!frame) return null\n const value = read(el)\n const fields = fieldsOf(el, root, core)\n const point = kind(el) === \"point\"\n return {\n core,\n root,\n grip,\n el,\n frame,\n keyboard,\n bounds: point ? boundsOf(el, frame) : frame,\n edge: attr(grip, \"box-handle\"),\n start: value,\n value,\n ratio: point ? 0 : ratioOf(el, frame, root, core),\n min: Number(attr(el, \"box-min\")) || 0,\n fields,\n before: Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.value])),\n }\n}\n\n// Write the fields now (set_value's write: `input` on each that moved).\nfunction flush(record) {\n cancelAnimationFrame(record.writing)\n record.writing = 0\n for (const [key, field] of Object.entries(record.fields)) record.core.setValue(field, String(round(record.value[key])))\n}\n\n// --- Pointer --------------------------------------------------------------------------\nfunction pointerDown(event, core, root) {\n forgetRemoved()\n if (drag || event.button > 0 || event.isPrimary === false || event.target.closest?.(FIELD)) return\n const grip = event.target.closest?.(GRIP)\n const record = grip && core.owns(grip) && begin(core, root, grip, false)\n if (!record) return\n const x0 = event.clientX\n const y0 = event.clientY\n const bounds = paddingBox(record.bounds)\n record.p0 = [(x0 - bounds.left) / bounds.width, (y0 - bounds.top) / bounds.height]\n // Only the pointer that pressed drives the drag: a second finger is not it.\n const mine = (e) => e.pointerId === event.pointerId\n const move = (e) => {\n // (A drag that ended before the release — its element left — stays over.)\n if (!mine(e) || record.over) return\n if (drag) return follow(e.clientX, e.clientY)\n if (Math.abs(e.clientX - x0) <= THRESHOLD && Math.abs(e.clientY - y0) <= THRESHOLD) return\n start(record)\n follow(e.clientX, e.clientY)\n }\n const up = (e) => {\n if (!mine(e)) return\n end()\n if (drag) suppressClick(), release()\n }\n const abort = (e) => {\n if (e.type === \"pointercancel\" && !mine(e)) return\n end()\n if (drag) cancel()\n }\n const escape = (e) => e.key === \"Escape\" && drag && (e.preventDefault(), abort(e))\n const on = [\n [\"pointermove\", move],\n [\"pointerup\", up],\n [\"pointercancel\", abort],\n [\"keydown\", escape],\n ]\n for (const [type, fn] of on) document.addEventListener(type, fn)\n function end() {\n for (const [type, fn] of on) document.removeEventListener(type, fn)\n }\n}\n\n// The click a pointer drag ends with is not one.\nfunction suppressClick() {\n const stop = (event) => (event.preventDefault(), event.stopPropagation())\n window.addEventListener(\"click\", stop, { capture: true, once: true })\n setTimeout(() => window.removeEventListener(\"click\", stop, { capture: true }))\n}\n\nfunction start(record) {\n drag = record\n record.grip.setAttribute(DRAGGING, \"\")\n record.frame.setAttribute(ACTIVE, \"\")\n record.select = document.documentElement.style.userSelect\n document.documentElement.style.userSelect = \"none\"\n}\n\n// The pointer is at (x, y): the element goes where the pointer's move since\n// the press puts it, measured in its bounds as they are NOW (a scroll moved them).\nfunction follow(x, y) {\n const record = drag\n // A morph or a stream took the element away: the drag is over.\n if (!record.el.isConnected) return settle(record)\n record.x = x\n record.y = y\n const bounds = paddingBox(record.bounds)\n const dx = (x - bounds.left) / bounds.width - record.p0[0]\n const dy = (y - bounds.top) / bounds.height - record.p0[1]\n const { start: from, edge } = record\n record.value = snap(\n kind(record.el) === \"point\"\n ? movePoint(from, dx, dy)\n : edge\n ? resizeBox(from, edge, dx, dy, record.min, record.ratio)\n : moveBox(from, dx, dy),\n )\n write(record.el, record.value)\n record.writing ||= requestAnimationFrame(() => flush(record))\n scroll(y)\n}\n\n// Near the window's top or bottom edge, scroll one step a frame until the\n// pointer leaves the edge (a frame taller than the window).\nfunction scroll(y) {\n drag.wy = y < EDGE ? -SPEED : y > window.innerHeight - EDGE ? SPEED : 0\n if (drag.wy && !drag.raf) {\n const step = () => {\n if (!drag?.wy) return drag && (drag.raf = 0)\n window.scrollBy(0, drag.wy)\n follow(drag.x, drag.y)\n if (drag) drag.raf = requestAnimationFrame(step)\n }\n drag.raf = requestAnimationFrame(step)\n }\n}\n\n// A drag whose element left the page (a stream replaced it while an arrow\n// was held: no keyup or focusout reaches it) is over, not in the way.\nfunction forgetRemoved() {\n if (drag && !drag.el.isConnected) settle(drag)\n}\n\n// --- Keyboard --------------------------------------------------------------------------\n// A focused box, handle or point: the arrows move it (Shift: ten times as\n// far), Alt+arrows resize a box from the focused handle or the bottom-right\n// corner, Escape puts it back. The key's release ends the step (`change`, the\n// action); each step is announced in the element's own words.\nfunction keyDown(event, core, root) {\n forgetRemoved()\n const grip = event.target\n // (A pointer drag, or a key held on another element, owns the drag.)\n if (!grip?.matches?.(GRIP) || !core.owns(grip) || (drag && (!drag.keyboard || drag.grip !== grip))) return\n if (event.key === \"Escape\") return drag && (event.preventDefault(), cancel())\n const arrow = ARROWS[event.key]\n if (!arrow) return\n event.preventDefault()\n drag ??= begin(core, root, grip, true)\n if (!drag) return\n const [dx, dy] = arrow.map((d) => d * (event.shiftKey ? 0.1 : 0.01))\n const resize = event.altKey && kind(drag.el) === \"box\"\n const now = drag.value\n drag.value = snap(\n kind(drag.el) === \"point\"\n ? movePoint(now, dx, dy)\n : resize\n ? resizeBox(now, drag.edge ?? \"se\", dx, dy, drag.min, drag.ratio)\n : moveBox(now, dx, dy),\n )\n write(drag.el, drag.value)\n flush(drag)\n announce(drag, resize ? \"resized\" : \"moved\")\n}\n\nfunction liveRegion() {\n if (!live?.isConnected) {\n live = document.createElement(\"div\")\n live.setAttribute(\"aria-live\", \"polite\")\n live.setAttribute(\"data-reactive-drag-live\", \"\")\n live.style.cssText = \"position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap\"\n document.body.append(live)\n }\n return live\n}\n\n// Clear the region now and write the words on the next frame, so a change is\n// heard even when the words are the same as the last ones.\nfunction announce(record, name) {\n let words = json(attr(record.el, `${kind(record.el)}-messages`), {})[name] ?? \"\"\n for (const key of keysOf(record.el)) words = words.replaceAll(`{${key}}`, Math.round(record.value[key] * 100))\n const region = liveRegion()\n region.textContent = \"\"\n cancelAnimationFrame(speaking)\n speaking = requestAnimationFrame(() => (region.textContent = words))\n}\n\n// --- Aspect ---------------------------------------------------------------------------\n// The field a box reads its ratio from changed (a preset radio was picked):\n// the box re-fits to it around its centre, inside the frame — a committed\n// change, like a release. A box already at that ratio stays as it is.\nfunction aspectChanged(event, core, root) {\n const name = event.target?.name\n if (!name) return\n for (const box of root.querySelectorAll(\"[data-reactive-box-aspect]\")) {\n if (attr(box, \"box-aspect\") !== name || box === drag?.el || !core.owns(box)) continue\n const record = begin(core, root, box, true)\n const { ratio, start: b } = record ?? {}\n if (!ratio || Math.abs(b.width / b.height / ratio - 1) < 0.005) continue\n record.value = snap(fitBox(b, ratio, record.min))\n write(box, record.value)\n flush(record)\n commit(record)\n }\n}\n\n// --- Release / cancel ---------------------------------------------------------------\nfunction release() {\n const record = drag\n // A morph took the element away with no move since: what it rendered stands.\n if (!record.el.isConnected) return settle(record)\n flush(record)\n settle(record)\n commit(record)\n}\n\n// One `change` on each field the drag moved, and the action, once, when the\n// element ended somewhere new.\nfunction commit(record) {\n for (const [key, field] of Object.entries(record.fields)) {\n if (field.value !== record.before[key]) field.dispatchEvent(new Event(\"change\", { bubbles: true }))\n }\n const name = kind(record.el)\n const action = attr(record.el, `${name}-action`)\n const params = {}\n for (const key of keysOf(record.el)) params[key] = round(record.value[key])\n if (!action || keysOf(record.el).every((key) => params[key] === round(record.start[key]))) return\n record.core.proceed(record.el, action, JSON.stringify(params), json(attr(record.el, `${name}-busy`)))\n}\n\nfunction cancel() {\n const record = drag\n record.value = record.start\n write(record.el, record.start)\n flush(record)\n if (record.keyboard) announce(record, \"cancelled\")\n settle(record)\n}\n\n// Undo everything the drag set: the markers, the frame loops, the text selection.\nfunction settle(record) {\n drag = null\n record.over = true\n cancelAnimationFrame(record.raf)\n cancelAnimationFrame(record.writing)\n record.grip.removeAttribute(DRAGGING)\n record.frame.removeAttribute(ACTIVE)\n if (!record.keyboard) document.documentElement.style.userSelect = record.select ?? \"\"\n}\n\n// --- Morphs ----------------------------------------------------------------------------\n// A morph that arrives mid-drag (a reply, a broadcast) does not move the\n// dragged element, write its fields or drop the markers: the drag wins until\n// release. A morph that moved a box or point outside a drag brings its fields\n// along — they may sit outside what the morph rendered.\nfunction holdMorphs() {\n if (morphs) return\n morphs = true\n document.addEventListener(\"turbo:before-morph-attribute\", (event) => {\n const el = event.target\n const name = event.detail?.attributeName\n if (!drag) return\n const held =\n (el === drag.el && name === \"style\") ||\n (el === drag.grip && name === DRAGGING) ||\n (el === drag.frame && name === ACTIVE) ||\n (name === \"value\" && Object.values(drag.fields).includes(el))\n if (held) event.preventDefault()\n })\n // (Every morphed element fires this: the cheap check first.)\n document.addEventListener(\"turbo:morph-element\", (event) => {\n const el = event.target\n if (el === drag?.el || !el.matches?.(`${BOX},${POINT}`)) return\n const root = el.closest(ROOT)\n const core = cores.get(root)\n if (!core?.owns(el)) return\n const value = read(el)\n for (const [key, field] of Object.entries(fieldsOf(el, root, core))) {\n if (round(Number(field.value)) !== round(value[key])) core.setValue(field, String(round(value[key])))\n }\n })\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "94F6177CA2EF4E8764756E2164756E21",
9
+ "names": []
10
+ }
@@ -33,6 +33,7 @@ import * as form from "phlex/reactive/features/form"
33
33
  import * as bindings from "phlex/reactive/features/bindings"
34
34
  import * as compute from "phlex/reactive/features/compute"
35
35
  import * as sortable from "phlex/reactive/features/sortable"
36
+ import * as drag from "phlex/reactive/features/drag"
36
37
  import * as presence from "phlex/reactive/features/presence"
37
38
  import * as countdown from "phlex/reactive/features/countdown"
38
39
  import * as effects from "phlex/reactive/features/effects"
@@ -67,6 +68,7 @@ if (__SPLIT__) {
67
68
  })
68
69
  registerReactiveFeature("compute", { connect: compute.connect, disconnect: compute.disconnect, seed: compute.seed, recompute: compute.recompute })
69
70
  registerReactiveFeature("sortable", { connect: sortable.connect, disconnect: sortable.disconnect })
71
+ registerReactiveFeature("drag", { connect: drag.connect, disconnect: drag.disconnect })
70
72
  registerReactiveFeature("presence", { connect: presence.connect, disconnect: presence.disconnect, set: presence.set })
71
73
  registerReactiveFeature("countdown", { connect: countdown.connect, disconnect: countdown.disconnect })
72
74
  registerReactiveFeature("effects", { wrap: effects.wrap, sweep: effects.sweep })