phlex-reactive 0.15.5 → 0.16.1
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 +344 -142
- data/README.md +100 -3
- data/app/controllers/phlex/reactive/actions_controller.rb +10 -0
- data/app/controllers/phlex/reactive/fragments_controller.rb +4 -3
- data/app/javascript/phlex/reactive/core.js +1 -0
- data/app/javascript/phlex/reactive/core.min.js +4 -4
- data/app/javascript/phlex/reactive/core.min.js.map +4 -4
- data/app/javascript/phlex/reactive/features/bindings.js +81 -4
- data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
- data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/drag.js +522 -0
- data/app/javascript/phlex/reactive/features/drag.min.js +4 -0
- data/app/javascript/phlex/reactive/features/drag.min.js.map +10 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +2 -0
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +7 -6
- data/app/javascript/phlex/reactive/runtime.js +57 -23
- data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +2 -1
- data/lib/phlex/reactive/apm/subscriber.rb +5 -0
- data/lib/phlex/reactive/apm.rb +5 -6
- data/lib/phlex/reactive/authorization.rb +31 -5
- data/lib/phlex/reactive/client_bindings.rb +19 -0
- data/lib/phlex/reactive/component/css_vars_binding.rb +118 -0
- data/lib/phlex/reactive/component/drag.rb +270 -0
- data/lib/phlex/reactive/component/dsl.rb +15 -1
- data/lib/phlex/reactive/component/helpers.rb +9 -1
- data/lib/phlex/reactive/component/identity.rb +17 -6
- data/lib/phlex/reactive/component/lazy.rb +4 -0
- data/lib/phlex/reactive/component/presence_declaration.rb +4 -0
- data/lib/phlex/reactive/component/registry.rb +76 -6
- data/lib/phlex/reactive/component/sortable.rb +3 -0
- data/lib/phlex/reactive/component.rb +4 -0
- data/lib/phlex/reactive/defer.rb +4 -4
- data/lib/phlex/reactive/doctor/ractor_readiness.rb +65 -0
- data/lib/phlex/reactive/doctor.rb +2 -0
- data/lib/phlex/reactive/engine.rb +31 -4
- data/lib/phlex/reactive/fragment.rb +5 -4
- data/lib/phlex/reactive/held_fields.rb +21 -1
- data/lib/phlex/reactive/param_schema.rb +3 -2
- data/lib/phlex/reactive/pending/markup.rb +27 -9
- data/lib/phlex/reactive/pending.rb +4 -4
- data/lib/phlex/reactive/response.rb +55 -4
- data/lib/phlex/reactive/shareable.rb +87 -0
- data/lib/phlex/reactive/store.rb +73 -0
- data/lib/phlex/reactive/streamable.rb +49 -19
- data/lib/phlex/reactive/test_helpers.rb +6 -0
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +293 -122
- data/sig/phlex/reactive/component.rbs +108 -0
- data/sig/phlex/reactive/param_schema.rbs +23 -0
- data/sig/phlex/reactive/response.rbs +73 -0
- data/sig/phlex/reactive/streamable.rbs +62 -0
- data/sig/phlex/reactive.rbs +231 -0
- metadata +14 -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 })
|