phlex-reactive 0.14.2 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +73 -5
- data/app/javascript/phlex/reactive/core.js +7 -1
- data/app/javascript/phlex/reactive/core.min.js +6 -2
- data/app/javascript/phlex/reactive/core.min.js.map +4 -4
- data/app/javascript/phlex/reactive/features/bindings.js +106 -2
- 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/form.js +12 -5
- data/app/javascript/phlex/reactive/features/form.min.js +2 -2
- data/app/javascript/phlex/reactive/features/form.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/persist.js +12 -3
- data/app/javascript/phlex/reactive/features/persist.min.js +2 -2
- data/app/javascript/phlex/reactive/features/persist.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/sortable.js +463 -0
- data/app/javascript/phlex/reactive/features/sortable.min.js +4 -0
- data/app/javascript/phlex/reactive/features/sortable.min.js.map +10 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +2 -0
- data/app/javascript/phlex/reactive/reactive_controller.min.js +6 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +9 -8
- data/app/javascript/phlex/reactive/runtime.js +117 -10
- data/lib/phlex/reactive/component/editable.rb +78 -0
- data/lib/phlex/reactive/component/helpers.rb +2 -0
- data/lib/phlex/reactive/component/sortable.rb +212 -0
- data/lib/phlex/reactive/engine.rb +4 -4
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +6 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// phlex/reactive/features/sortable — drag rows to reorder a list, or drag a
|
|
2
|
+
// row in from another list, and send ONE declared action with the row and its
|
|
3
|
+
// new position (issue #350). One of the feature modules the core imports on
|
|
4
|
+
// demand (issue #275); in the default client it is part of the one file.
|
|
5
|
+
//
|
|
6
|
+
// The markup is the Ruby helpers' (Phlex::Reactive::Component::Sortable):
|
|
7
|
+
//
|
|
8
|
+
// data-reactive-sortable="move" a list; the action a drop calls
|
|
9
|
+
// -group / -handle / -name / -from / -params / -disabled / -busy / -messages
|
|
10
|
+
// data-reactive-sortable-source="name" a list whose rows are COPIED in
|
|
11
|
+
// data-reactive-sortable-item='7' a row and its JSON-typed key
|
|
12
|
+
//
|
|
13
|
+
// and what it sets while a drag is on, for the app's CSS:
|
|
14
|
+
//
|
|
15
|
+
// data-reactive-sortable-active on the list being dragged from/over
|
|
16
|
+
// data-reactive-sortable-dragging on the dragged row (or its clone)
|
|
17
|
+
// data-reactive-sortable-placeholder the gap that marks the drop point
|
|
18
|
+
//
|
|
19
|
+
// Input is pointer events, not HTML5 drag and drop: mouse, pen and touch
|
|
20
|
+
// behave the same, the dragged row is the real row under the pointer (no
|
|
21
|
+
// ghost image), and the keyboard gets the same moves. A drag starts after a
|
|
22
|
+
// 4 px move, or on touch after a 200 ms press, so taps, clicks and scrolling
|
|
23
|
+
// still work.
|
|
24
|
+
//
|
|
25
|
+
// A drop moves the row in the DOM at once (a drag-in inserts nothing: the
|
|
26
|
+
// target's action decides and its reply renders the row), then sends the
|
|
27
|
+
// action through the root's own pipeline (core.proceed): the signed token,
|
|
28
|
+
// the request queue, the busy state, the reply. The row goes back to where it
|
|
29
|
+
// was when the request fails, is vetoed, or its reply does not render the
|
|
30
|
+
// list. A reply that does render it (a morph) puts the rows in the server's
|
|
31
|
+
// order: Idiomorph matches them by id.
|
|
32
|
+
//
|
|
33
|
+
// The geometry is every element's own getBoundingClientRect() — nothing is
|
|
34
|
+
// cached across moves — so a test without layout stubs it per element.
|
|
35
|
+
//
|
|
36
|
+
// This module never imports the core: it reaches a controller through the
|
|
37
|
+
// `core` handle (core.owns, core.proceed).
|
|
38
|
+
|
|
39
|
+
const LIST = "[data-reactive-sortable]"
|
|
40
|
+
const SOURCE = "[data-reactive-sortable-source]"
|
|
41
|
+
const ITEM = "[data-reactive-sortable-item]"
|
|
42
|
+
const ROOT = '[data-controller~="reactive"]'
|
|
43
|
+
const FIELD = "input,textarea,select,[contenteditable]"
|
|
44
|
+
const THRESHOLD = 4 // px a press must move before it is a drag
|
|
45
|
+
const PRESS_MS = 200 // how long a touch is held before it picks a row up
|
|
46
|
+
const EDGE = 40 // px from an edge where auto-scroll starts
|
|
47
|
+
const SPEED = 12 // px auto-scroll moves per frame
|
|
48
|
+
|
|
49
|
+
// controller -> the removals of what connect() added.
|
|
50
|
+
const wired = new WeakMap()
|
|
51
|
+
// reactive root element -> its core handle: a drop into a list of ANOTHER root
|
|
52
|
+
// (a drag-in, a move between roots of one group) sends through that root.
|
|
53
|
+
const cores = new WeakMap()
|
|
54
|
+
// The one drag on the page, by pointer or keyboard, or null.
|
|
55
|
+
let drag = null
|
|
56
|
+
// The page's live region (re-created after a Turbo visit replaced <body>),
|
|
57
|
+
// and the frame that will write its next words.
|
|
58
|
+
let live
|
|
59
|
+
let speaking
|
|
60
|
+
|
|
61
|
+
export function connect(controller, core) {
|
|
62
|
+
const root = controller.element
|
|
63
|
+
cores.set(root, core)
|
|
64
|
+
// The live region is on the page, empty, before anything is announced: a
|
|
65
|
+
// screen reader often ignores a region that arrives already holding words.
|
|
66
|
+
liveRegion()
|
|
67
|
+
const press = (event) => pointerDown(event, core)
|
|
68
|
+
const key = (event) => keyDown(event, core)
|
|
69
|
+
root.addEventListener("pointerdown", press)
|
|
70
|
+
root.addEventListener("keydown", key)
|
|
71
|
+
wired.set(controller, () => {
|
|
72
|
+
root.removeEventListener("pointerdown", press)
|
|
73
|
+
root.removeEventListener("keydown", key)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function disconnect(controller) {
|
|
78
|
+
wired.get(controller)?.()
|
|
79
|
+
wired.delete(controller)
|
|
80
|
+
cores.delete(controller.element)
|
|
81
|
+
// A root that leaves mid-drag (a replace, a navigation) ends its drag.
|
|
82
|
+
if (drag && rootOf(drag.origin) === controller.element) cancel()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const attr = (el, name = "") => el?.getAttribute?.(`data-reactive-sortable${name}`)
|
|
86
|
+
const json = (value, fallback) => {
|
|
87
|
+
try {
|
|
88
|
+
return value == null ? fallback : JSON.parse(value)
|
|
89
|
+
} catch {
|
|
90
|
+
return fallback
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const rootOf = (el) => el?.closest?.(ROOT)
|
|
94
|
+
const containerOf = (item) => item.parentElement?.closest(`${LIST},${SOURCE}`)
|
|
95
|
+
// A list's rows: its items whose closest list is this one (a list nested in a
|
|
96
|
+
// row keeps its own rows), minus the one being dragged.
|
|
97
|
+
const itemsOf = (list) =>
|
|
98
|
+
[...list.querySelectorAll(ITEM)].filter((el) => el !== drag?.row && containerOf(el) === list)
|
|
99
|
+
const keyOf = (item) => json(attr(item, "-item"), null)
|
|
100
|
+
const disabled = (el) => attr(el, "-disabled") === "true"
|
|
101
|
+
const inside = (el, x, y) => {
|
|
102
|
+
const rect = el.getBoundingClientRect()
|
|
103
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
|
|
104
|
+
}
|
|
105
|
+
// The lists a row from `origin` can be dropped into: every enabled sortable of
|
|
106
|
+
// its group whose root has a core, or (no group) the list itself.
|
|
107
|
+
const targetsFor = (origin) => {
|
|
108
|
+
const group = attr(origin, "-group")
|
|
109
|
+
if (!group) return origin.matches(LIST) ? [origin] : []
|
|
110
|
+
return [...document.querySelectorAll(LIST)].filter(
|
|
111
|
+
(list) => attr(list, "-group") === group && !disabled(list) && cores.has(rootOf(list)),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The row a gesture on `target` may pick up, or nothing: a row of a list this
|
|
116
|
+
// root owns (a nested root's rows are its own, issue #15), neither of them
|
|
117
|
+
// disabled, and no request of that root in flight.
|
|
118
|
+
function grab(target, core) {
|
|
119
|
+
const item = target?.closest?.(ITEM)
|
|
120
|
+
const list = item && containerOf(item)
|
|
121
|
+
if (drag || !list || !core.owns(list) || disabled(list) || disabled(item)) return
|
|
122
|
+
if (rootOf(list)?.getAttribute("aria-busy") === "true") return
|
|
123
|
+
return { item, list }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Pointer ----------------------------------------------------------------
|
|
127
|
+
function pointerDown(event, core) {
|
|
128
|
+
if (event.button > 0 || event.isPrimary === false) return
|
|
129
|
+
const grabbed = grab(event.target, core)
|
|
130
|
+
if (!grabbed) return
|
|
131
|
+
const handle = attr(grabbed.list, "-handle")
|
|
132
|
+
if (handle ? !grabbed.item.contains(event.target.closest(handle)) : event.target.closest(FIELD)) return
|
|
133
|
+
|
|
134
|
+
const x0 = event.clientX
|
|
135
|
+
const y0 = event.clientY
|
|
136
|
+
// Only the pointer that pressed drives the drag: a second finger is not it.
|
|
137
|
+
const mine = (e) => e.pointerId === event.pointerId
|
|
138
|
+
const touch = event.pointerType === "touch"
|
|
139
|
+
let ready = !touch
|
|
140
|
+
const timer = touch && setTimeout(() => (ready = true), PRESS_MS)
|
|
141
|
+
const move = (e) => {
|
|
142
|
+
if (!mine(e)) return
|
|
143
|
+
if (drag) return follow(e.clientX, e.clientY)
|
|
144
|
+
if (Math.abs(e.clientX - x0) <= THRESHOLD && Math.abs(e.clientY - y0) <= THRESHOLD) return
|
|
145
|
+
// A finger that moves before the press is up is scrolling.
|
|
146
|
+
if (!ready) return end()
|
|
147
|
+
start(grabbed, core, e)
|
|
148
|
+
follow(e.clientX, e.clientY)
|
|
149
|
+
}
|
|
150
|
+
// While a touch drag is on, the page must not scroll under it.
|
|
151
|
+
const hold = (e) => drag && e.cancelable && e.preventDefault()
|
|
152
|
+
const up = (e) => {
|
|
153
|
+
if (!mine(e)) return
|
|
154
|
+
end()
|
|
155
|
+
if (drag) suppressClick(), drop()
|
|
156
|
+
}
|
|
157
|
+
const abort = (e) => {
|
|
158
|
+
if (e.type === "pointercancel" && !mine(e)) return
|
|
159
|
+
end()
|
|
160
|
+
if (drag) cancel()
|
|
161
|
+
}
|
|
162
|
+
const escape = (e) => e.key === "Escape" && drag && (e.preventDefault(), abort(e))
|
|
163
|
+
const on = [
|
|
164
|
+
["pointermove", move],
|
|
165
|
+
["pointerup", up],
|
|
166
|
+
["pointercancel", abort],
|
|
167
|
+
["keydown", escape],
|
|
168
|
+
["touchmove", hold, { passive: false }],
|
|
169
|
+
]
|
|
170
|
+
for (const [type, fn, options] of on) document.addEventListener(type, fn, options)
|
|
171
|
+
function end() {
|
|
172
|
+
clearTimeout(timer)
|
|
173
|
+
for (const [type, fn] of on) document.removeEventListener(type, fn)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The click a pointer drag ends with (over a button in the row) is not one.
|
|
178
|
+
function suppressClick() {
|
|
179
|
+
const stop = (event) => (event.preventDefault(), event.stopPropagation())
|
|
180
|
+
window.addEventListener("click", stop, { capture: true, once: true })
|
|
181
|
+
setTimeout(() => window.removeEventListener("click", stop, { capture: true }))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Lift the row (a source's row: a clone of it) under the pointer, and leave
|
|
185
|
+
// a placeholder of its height where it was.
|
|
186
|
+
function start({ item, list }, core, event) {
|
|
187
|
+
const rect = item.getBoundingClientRect()
|
|
188
|
+
const source = list.matches(SOURCE)
|
|
189
|
+
const row = source ? item.cloneNode(true) : item
|
|
190
|
+
if (source) {
|
|
191
|
+
// A clone must not connect a controller or duplicate an id.
|
|
192
|
+
for (const el of [row, ...row.querySelectorAll("[id],[data-controller]")]) {
|
|
193
|
+
el.removeAttribute("id")
|
|
194
|
+
el.removeAttribute("data-controller")
|
|
195
|
+
}
|
|
196
|
+
document.body.append(row)
|
|
197
|
+
}
|
|
198
|
+
const placeholder = document.createElement(item.tagName)
|
|
199
|
+
placeholder.setAttribute("data-reactive-sortable-placeholder", "")
|
|
200
|
+
placeholder.setAttribute("aria-hidden", "true")
|
|
201
|
+
placeholder.style.height = `${rect.height}px`
|
|
202
|
+
drag = begin({ core, item, row, origin: list, source, placeholder, style: row.getAttribute("style") })
|
|
203
|
+
drag.dx = event.clientX - rect.left
|
|
204
|
+
drag.dy = event.clientY - rect.top
|
|
205
|
+
drag.select = document.documentElement.style.userSelect
|
|
206
|
+
document.documentElement.style.userSelect = "none"
|
|
207
|
+
if (!source) item.before(placeholder)
|
|
208
|
+
Object.assign(row.style, {
|
|
209
|
+
position: "fixed",
|
|
210
|
+
left: `${rect.left}px`,
|
|
211
|
+
top: `${rect.top}px`,
|
|
212
|
+
width: `${rect.width}px`,
|
|
213
|
+
height: `${rect.height}px`,
|
|
214
|
+
margin: "0",
|
|
215
|
+
boxSizing: "border-box",
|
|
216
|
+
zIndex: "2147483647",
|
|
217
|
+
pointerEvents: "none",
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The drag record both inputs share; marks the row and its list.
|
|
222
|
+
function begin(record) {
|
|
223
|
+
const { row, origin, source } = record
|
|
224
|
+
record.index = itemsOf(origin).indexOf(record.item)
|
|
225
|
+
record.target = source ? null : origin
|
|
226
|
+
row.setAttribute("data-reactive-sortable-dragging", "")
|
|
227
|
+
activate(origin, true)
|
|
228
|
+
return record
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const activate = (list, on) => list?.toggleAttribute("data-reactive-sortable-active", on)
|
|
232
|
+
|
|
233
|
+
// Put `el` at `index` among a list's rows (`items`, which leave `el` out):
|
|
234
|
+
// before the row there, or after the last one, or into the empty list.
|
|
235
|
+
function insertAt(list, items, index, el) {
|
|
236
|
+
if (items[index]) items[index].before(el)
|
|
237
|
+
else if (items.length) items.at(-1).after(el)
|
|
238
|
+
else list.append(el)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function follow(x, y) {
|
|
242
|
+
drag.x = x
|
|
243
|
+
drag.y = y
|
|
244
|
+
drag.row.style.left = `${x - drag.dx}px`
|
|
245
|
+
drag.row.style.top = `${y - drag.dy}px`
|
|
246
|
+
const list = targetsFor(drag.origin).find((el) => inside(el, x, y))
|
|
247
|
+
if (list) place(list, x, y)
|
|
248
|
+
// Off every list, a drag-in has nowhere to land; a move keeps its last spot.
|
|
249
|
+
else if (drag.source) retarget(null), drag.placeholder.remove()
|
|
250
|
+
scroll(x, y)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function retarget(list) {
|
|
254
|
+
if (drag.target !== drag.origin) activate(drag.target, false)
|
|
255
|
+
drag.target = list
|
|
256
|
+
activate(list, true)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The placeholder goes before the first row whose middle is past the pointer.
|
|
260
|
+
// A list whose first two rows share a top is laid out across, not down.
|
|
261
|
+
function place(list, x, y) {
|
|
262
|
+
retarget(list)
|
|
263
|
+
const items = itemsOf(list)
|
|
264
|
+
const [a, b] = items.map((el) => el.getBoundingClientRect())
|
|
265
|
+
const across = b && a.top === b.top && a.left !== b.left
|
|
266
|
+
const index = items.findIndex((el) => {
|
|
267
|
+
const rect = el.getBoundingClientRect()
|
|
268
|
+
return across ? x < rect.left + rect.width / 2 : y < rect.top + rect.height / 2
|
|
269
|
+
})
|
|
270
|
+
insertAt(list, items, index < 0 ? items.length : index, drag.placeholder)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Near the top or bottom edge of the list being dragged over (when it
|
|
274
|
+
// scrolls) or of the window, scroll one step a frame until the pointer leaves
|
|
275
|
+
// the edge.
|
|
276
|
+
function scroll(x, y) {
|
|
277
|
+
const list = drag.target ?? drag.origin
|
|
278
|
+
const rect = list.getBoundingClientRect()
|
|
279
|
+
const scrolls = list.scrollHeight > list.clientHeight
|
|
280
|
+
drag.vy = scrolls ? (y < rect.top + EDGE ? -SPEED : y > rect.bottom - EDGE ? SPEED : 0) : 0
|
|
281
|
+
drag.wy = y < EDGE ? -SPEED : y > window.innerHeight - EDGE ? SPEED : 0
|
|
282
|
+
if ((drag.vy || drag.wy) && !drag.raf) {
|
|
283
|
+
const step = () => {
|
|
284
|
+
if (!drag || !(drag.vy || drag.wy)) return drag && (drag.raf = 0)
|
|
285
|
+
;(drag.target ?? drag.origin).scrollTop += drag.vy
|
|
286
|
+
if (drag.wy) window.scrollBy(0, drag.wy)
|
|
287
|
+
follow(drag.x, drag.y)
|
|
288
|
+
drag.raf = requestAnimationFrame(step)
|
|
289
|
+
}
|
|
290
|
+
drag.raf = requestAnimationFrame(step)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- Keyboard ----------------------------------------------------------------
|
|
295
|
+
// A focused row: Space/Enter picks it up, the arrows move it (← / → into the
|
|
296
|
+
// previous / next list of its group), Space/Enter drops it, Escape (or Tab)
|
|
297
|
+
// puts it back. Each step is announced in the list's own words.
|
|
298
|
+
function keyDown(event, core) {
|
|
299
|
+
const key = event.key
|
|
300
|
+
if (drag?.keyboard) {
|
|
301
|
+
// (A nested root's row bubbles to every ancestor root: only the root that
|
|
302
|
+
// picked the row up answers.)
|
|
303
|
+
if (event.target !== drag.row || drag.core !== core) return
|
|
304
|
+
const handled = {
|
|
305
|
+
" ": drop,
|
|
306
|
+
Enter: drop,
|
|
307
|
+
Escape: cancel,
|
|
308
|
+
Tab: cancel,
|
|
309
|
+
ArrowUp: () => step(-1),
|
|
310
|
+
ArrowDown: () => step(1),
|
|
311
|
+
ArrowLeft: () => hop(-1),
|
|
312
|
+
ArrowRight: () => hop(1),
|
|
313
|
+
}[key]
|
|
314
|
+
if (!handled) return
|
|
315
|
+
if (key !== "Tab") event.preventDefault()
|
|
316
|
+
return handled()
|
|
317
|
+
}
|
|
318
|
+
if (key !== " " && key !== "Enter") return
|
|
319
|
+
const grabbed = grab(event.target, core)
|
|
320
|
+
if (!grabbed || grabbed.item !== event.target || grabbed.list.matches(SOURCE)) return
|
|
321
|
+
event.preventDefault()
|
|
322
|
+
drag = begin({ core, keyboard: true, item: grabbed.item, row: grabbed.item, origin: grabbed.list, style: grabbed.item.getAttribute("style") })
|
|
323
|
+
announce("picked")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function step(by) {
|
|
327
|
+
const items = itemsOf(drag.target)
|
|
328
|
+
// (itemsOf leaves the dragged row out: it sits before items[index].)
|
|
329
|
+
const to = position() + by
|
|
330
|
+
// Past either end the row stays, and where it is is said again.
|
|
331
|
+
if (to >= 0 && to <= items.length) insertAt(drag.target, items, to, drag.row)
|
|
332
|
+
moved()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function hop(by) {
|
|
336
|
+
const lists = targetsFor(drag.origin)
|
|
337
|
+
const list = lists[lists.indexOf(drag.target) + by]
|
|
338
|
+
if (list) {
|
|
339
|
+
const index = position()
|
|
340
|
+
retarget(list)
|
|
341
|
+
insertAt(list, itemsOf(list), index, drag.row)
|
|
342
|
+
}
|
|
343
|
+
moved()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function moved() {
|
|
347
|
+
drag.row.focus?.()
|
|
348
|
+
announce("moved")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// The dragged row's index in its target list (the drop's `position`).
|
|
352
|
+
function position() {
|
|
353
|
+
const items = itemsOf(drag.target)
|
|
354
|
+
const row = drag.keyboard ? drag.row : drag.placeholder
|
|
355
|
+
// (itemsOf leaves the dragged row out: count the rows before it.)
|
|
356
|
+
return items.filter((el) => el.compareDocumentPosition(row) & 4).length
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function liveRegion() {
|
|
360
|
+
if (!live?.isConnected) {
|
|
361
|
+
live = document.createElement("div")
|
|
362
|
+
live.setAttribute("aria-live", "polite")
|
|
363
|
+
live.setAttribute("data-reactive-sortable-live", "")
|
|
364
|
+
live.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap"
|
|
365
|
+
document.body.append(live)
|
|
366
|
+
}
|
|
367
|
+
return live
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Clear the region now and write the words on the next frame, so a change is
|
|
371
|
+
// heard even when the words are the same as the last ones.
|
|
372
|
+
function announce(name) {
|
|
373
|
+
const list = drag.target ?? drag.origin
|
|
374
|
+
const words = (json(attr(list, "-messages"), {})[name] ?? "")
|
|
375
|
+
.replaceAll("{position}", position() + 1)
|
|
376
|
+
.replaceAll("{count}", itemsOf(list).length + 1)
|
|
377
|
+
const region = liveRegion()
|
|
378
|
+
region.textContent = ""
|
|
379
|
+
cancelAnimationFrame(speaking)
|
|
380
|
+
speaking = requestAnimationFrame(() => (region.textContent = words))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- Drop / cancel -----------------------------------------------------------
|
|
384
|
+
// Settle the row, then send the target's action: a move that changed nothing
|
|
385
|
+
// sends nothing. Returns before the reply; the row goes back if it fails.
|
|
386
|
+
function drop() {
|
|
387
|
+
const record = drag
|
|
388
|
+
const { target, origin, source, row, placeholder } = record
|
|
389
|
+
const landed = target && (source ? placeholder.isConnected : true)
|
|
390
|
+
const index = landed ? position() : -1
|
|
391
|
+
const after = landed ? keyOf(itemsOf(target)[index - 1]) : null
|
|
392
|
+
if (record.keyboard) announce("dropped")
|
|
393
|
+
if (!source && placeholder) placeholder.replaceWith(row)
|
|
394
|
+
settle(record)
|
|
395
|
+
if (!landed || (target === origin && index === record.index)) return
|
|
396
|
+
|
|
397
|
+
const from = target === origin ? "self" : source ? attr(origin, "-source") : (attr(origin, "-name") ?? "list")
|
|
398
|
+
const action = json(attr(target, "-from"), {})[from] ?? attr(target)
|
|
399
|
+
const params = { ...json(attr(target, "-params"), {}), item: keyOf(record.item), position: index, after, from }
|
|
400
|
+
// The list and its ancestors: a reply stream that replaces or updates one
|
|
401
|
+
// of them (by id, or by a `targets:` selector) rendered the list (read now —
|
|
402
|
+
// a replace detaches it).
|
|
403
|
+
const chain = []
|
|
404
|
+
for (let el = target; el; el = el.parentElement) chain.push(el)
|
|
405
|
+
const sent = cores.get(rootOf(target))?.proceed(target, action, JSON.stringify(params), json(attr(target, "-busy")))
|
|
406
|
+
if (source) return
|
|
407
|
+
Promise.resolve(sent).then((html) => renders(html, chain) || restore(record))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function renders(html, chain) {
|
|
411
|
+
if (!html) return false
|
|
412
|
+
const template = document.createElement("template")
|
|
413
|
+
template.innerHTML = html
|
|
414
|
+
return [...template.content.querySelectorAll("turbo-stream")].some(
|
|
415
|
+
(stream) => /^(replace|update)$/.test(stream.getAttribute("action")) && hits(stream, chain),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function hits(stream, chain) {
|
|
420
|
+
const target = stream.getAttribute("target")
|
|
421
|
+
if (target) return chain.some((el) => el.id === target)
|
|
422
|
+
const selector = stream.getAttribute("targets")
|
|
423
|
+
try {
|
|
424
|
+
return Boolean(selector) && chain.some((el) => el.matches(selector))
|
|
425
|
+
} catch {
|
|
426
|
+
return false // an invalid selector renders nothing
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function cancel() {
|
|
431
|
+
const record = drag
|
|
432
|
+
if (record.keyboard) announce("cancelled")
|
|
433
|
+
record.placeholder?.remove()
|
|
434
|
+
settle(record)
|
|
435
|
+
restore(record)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Put a moved row back at its original index in its original list.
|
|
439
|
+
function restore({ row, origin, index, source }) {
|
|
440
|
+
if (source || !row.isConnected) return
|
|
441
|
+
insertAt(
|
|
442
|
+
origin,
|
|
443
|
+
itemsOf(origin).filter((el) => el !== row),
|
|
444
|
+
index,
|
|
445
|
+
row,
|
|
446
|
+
)
|
|
447
|
+
if (row === document.activeElement) row.focus?.()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Undo everything the drag set: the row's style and marker, the lists'
|
|
451
|
+
// markers, the clone, the placeholder, the frame loop, the text selection.
|
|
452
|
+
function settle(record) {
|
|
453
|
+
const { row, origin, target, source, style, placeholder } = record
|
|
454
|
+
drag = null
|
|
455
|
+
if (record.raf) cancelAnimationFrame(record.raf)
|
|
456
|
+
row.removeAttribute("data-reactive-sortable-dragging")
|
|
457
|
+
if (source) row.remove()
|
|
458
|
+
else if (!record.keyboard) style == null ? row.removeAttribute("style") : row.setAttribute("style", style)
|
|
459
|
+
placeholder?.remove()
|
|
460
|
+
activate(origin, false)
|
|
461
|
+
activate(target, false)
|
|
462
|
+
if (!record.keyboard) document.documentElement.style.userSelect = record.select ?? ""
|
|
463
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var R=new WeakMap,v=new WeakMap,o=null,m,F;function it(t,e){let n=t.element;v.set(n,e),j();let r=(s)=>Q(s,e),i=(s)=>nt(s,e);n.addEventListener("pointerdown",r),n.addEventListener("keydown",i),R.set(t,()=>{n.removeEventListener("pointerdown",r),n.removeEventListener("keydown",i)})}function ct(t){if(R.get(t)?.(),R.delete(t),v.delete(t.element),o&&A(o.origin)===t.element)w()}var u=(t,e="")=>t?.getAttribute?.(`data-reactive-sortable${e}`),b=(t,e)=>{try{return t==null?e:JSON.parse(t)}catch{return e}},A=(t)=>t?.closest?.('[data-controller~="reactive"]'),U=(t)=>t.parentElement?.closest("[data-reactive-sortable],[data-reactive-sortable-source]"),p=(t)=>[...t.querySelectorAll("[data-reactive-sortable-item]")].filter((e)=>e!==o?.row&&U(e)===t),q=(t)=>b(u(t,"-item"),null),C=(t)=>u(t,"-disabled")==="true",K=(t,e,n)=>{let r=t.getBoundingClientRect();return e>=r.left&&e<=r.right&&n>=r.top&&n<=r.bottom},X=(t)=>{let e=u(t,"-group");if(!e)return t.matches("[data-reactive-sortable]")?[t]:[];return[...document.querySelectorAll("[data-reactive-sortable]")].filter((n)=>u(n,"-group")===e&&!C(n)&&v.has(A(n)))};function Y(t,e){let n=t?.closest?.("[data-reactive-sortable-item]"),r=n&&U(n);if(o||!r||!e.owns(r)||C(r)||C(n))return;if(A(r)?.getAttribute("aria-busy")==="true")return;return{item:n,list:r}}function Q(t,e){if(t.button>0||t.isPrimary===!1)return;let n=Y(t.target,e);if(!n)return;let r=u(n.list,"-handle");if(r?!n.item.contains(t.target.closest(r)):t.target.closest("input,textarea,select,[contenteditable]"))return;let{clientX:i,clientY:s}=t,a=(c)=>c.pointerId===t.pointerId,l=t.pointerType==="touch",f=!l,d=l&&setTimeout(()=>f=!0,200),k=(c)=>{if(!a(c))return;if(o)return I(c.clientX,c.clientY);if(Math.abs(c.clientX-i)<=4&&Math.abs(c.clientY-s)<=4)return;if(!f)return O();Z(n,e,c),I(c.clientX,c.clientY)},T=(c)=>o&&c.cancelable&&c.preventDefault(),E=(c)=>{if(!a(c))return;if(O(),o)V(),P()},y=(c)=>{if(c.type==="pointercancel"&&!a(c))return;if(O(),o)w()},M=[["pointermove",k],["pointerup",E],["pointercancel",y],["keydown",(c)=>c.key==="Escape"&&o&&(c.preventDefault(),y(c))],["touchmove",T,{passive:!1}]];for(let[c,L,_]of M)document.addEventListener(c,L,_);function O(){clearTimeout(d);for(let[c,L]of M)document.removeEventListener(c,L)}}function V(){let t=(e)=>(e.preventDefault(),e.stopPropagation());window.addEventListener("click",t,{capture:!0,once:!0}),setTimeout(()=>window.removeEventListener("click",t,{capture:!0}))}function Z({item:t,list:e},n,r){let i=t.getBoundingClientRect(),s=e.matches("[data-reactive-sortable-source]"),a=s?t.cloneNode(!0):t;if(s){for(let f of[a,...a.querySelectorAll("[id],[data-controller]")])f.removeAttribute("id"),f.removeAttribute("data-controller");document.body.append(a)}let l=document.createElement(t.tagName);if(l.setAttribute("data-reactive-sortable-placeholder",""),l.setAttribute("aria-hidden","true"),l.style.height=`${i.height}px`,o=N({core:n,item:t,row:a,origin:e,source:s,placeholder:l,style:a.getAttribute("style")}),o.dx=r.clientX-i.left,o.dy=r.clientY-i.top,o.select=document.documentElement.style.userSelect,document.documentElement.style.userSelect="none",!s)t.before(l);Object.assign(a.style,{position:"fixed",left:`${i.left}px`,top:`${i.top}px`,width:`${i.width}px`,height:`${i.height}px`,margin:"0",boxSizing:"border-box",zIndex:"2147483647",pointerEvents:"none"})}function N(t){let{row:e,origin:n,source:r}=t;return t.index=p(n).indexOf(t.item),t.target=r?null:n,e.setAttribute("data-reactive-sortable-dragging",""),h(n,!0),t}var h=(t,e)=>t?.toggleAttribute("data-reactive-sortable-active",e);function S(t,e,n,r){if(e[n])e[n].before(r);else if(e.length)e.at(-1).after(r);else t.append(r)}function I(t,e){o.x=t,o.y=e,o.row.style.left=`${t-o.dx}px`,o.row.style.top=`${e-o.dy}px`;let n=X(o.origin).find((r)=>K(r,t,e));if(n)tt(n,t,e);else if(o.source)H(null),o.placeholder.remove();et(t,e)}function H(t){if(o.target!==o.origin)h(o.target,!1);o.target=t,h(t,!0)}function tt(t,e,n){H(t);let r=p(t),[i,s]=r.map((f)=>f.getBoundingClientRect()),a=s&&i.top===s.top&&i.left!==s.left,l=r.findIndex((f)=>{let d=f.getBoundingClientRect();return a?e<d.left+d.width/2:n<d.top+d.height/2});S(t,r,l<0?r.length:l,o.placeholder)}function et(t,e){let n=o.target??o.origin,r=n.getBoundingClientRect(),i=n.scrollHeight>n.clientHeight;if(o.vy=i?e<r.top+40?-12:e>r.bottom-40?12:0:0,o.wy=e<40?-12:e>window.innerHeight-40?12:0,(o.vy||o.wy)&&!o.raf){let s=()=>{if(!o||!(o.vy||o.wy))return o&&(o.raf=0);if((o.target??o.origin).scrollTop+=o.vy,o.wy)window.scrollBy(0,o.wy);I(o.x,o.y),o.raf=requestAnimationFrame(s)};o.raf=requestAnimationFrame(s)}}function nt(t,e){let n=t.key;if(o?.keyboard){if(t.target!==o.row||o.core!==e)return;let i={" ":P,Enter:P,Escape:w,Tab:w,ArrowUp:()=>B(-1),ArrowDown:()=>B(1),ArrowLeft:()=>G(-1),ArrowRight:()=>G(1)}[n];if(!i)return;if(n!=="Tab")t.preventDefault();return i()}if(n!==" "&&n!=="Enter")return;let r=Y(t.target,e);if(!r||r.item!==t.target||r.list.matches("[data-reactive-sortable-source]"))return;t.preventDefault(),o=N({core:e,keyboard:!0,item:r.item,row:r.item,origin:r.list,style:r.item.getAttribute("style")}),D("picked")}function B(t){let e=p(o.target),n=x()+t;if(n>=0&&n<=e.length)S(o.target,e,n,o.row);W()}function G(t){let e=X(o.origin),n=e[e.indexOf(o.target)+t];if(n){let r=x();H(n),S(n,p(n),r,o.row)}W()}function W(){o.row.focus?.(),D("moved")}function x(){let t=p(o.target),e=o.keyboard?o.row:o.placeholder;return t.filter((n)=>n.compareDocumentPosition(e)&4).length}function j(){if(!m?.isConnected)m=document.createElement("div"),m.setAttribute("aria-live","polite"),m.setAttribute("data-reactive-sortable-live",""),m.style.cssText="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap",document.body.append(m);return m}function D(t){let e=o.target??o.origin,n=(b(u(e,"-messages"),{})[t]??"").replaceAll("{position}",x()+1).replaceAll("{count}",p(e).length+1),r=j();r.textContent="",cancelAnimationFrame(F),F=requestAnimationFrame(()=>r.textContent=n)}function P(){let t=o,{target:e,origin:n,source:r,row:i,placeholder:s}=t,a=e&&(r?s.isConnected:!0),l=a?x():-1,f=a?q(p(e)[l-1]):null;if(t.keyboard)D("dropped");if(!r&&s)s.replaceWith(i);if(J(t),!a||e===n&&l===t.index)return;let d=e===n?"self":r?u(n,"-source"):u(n,"-name")??"list",k=b(u(e,"-from"),{})[d]??u(e),T={...b(u(e,"-params"),{}),item:q(t.item),position:l,after:f,from:d},E=[];for(let g=e;g;g=g.parentElement)E.push(g);let y=v.get(A(e))?.proceed(e,k,JSON.stringify(T),b(u(e,"-busy")));if(r)return;Promise.resolve(y).then((g)=>ot(g,E)||z(t))}function ot(t,e){if(!t)return!1;let n=document.createElement("template");return n.innerHTML=t,[...n.content.querySelectorAll("turbo-stream")].some((r)=>/^(replace|update)$/.test(r.getAttribute("action"))&&rt(r,e))}function rt(t,e){let n=t.getAttribute("target");if(n)return e.some((i)=>i.id===n);let r=t.getAttribute("targets");try{return Boolean(r)&&e.some((i)=>i.matches(r))}catch{return!1}}function w(){let t=o;if(t.keyboard)D("cancelled");t.placeholder?.remove(),J(t),z(t)}function z({row:t,origin:e,index:n,source:r}){if(r||!t.isConnected)return;if(S(e,p(e).filter((i)=>i!==t),n,t),t===document.activeElement)t.focus?.()}function J(t){let{row:e,origin:n,target:r,source:i,style:s,placeholder:a}=t;if(o=null,t.raf)cancelAnimationFrame(t.raf);if(e.removeAttribute("data-reactive-sortable-dragging"),i)e.remove();else if(!t.keyboard)s==null?e.removeAttribute("style"):e.setAttribute("style",s);if(a?.remove(),h(n,!1),h(r,!1),!t.keyboard)document.documentElement.style.userSelect=t.select??""}export{it as connect,ct as disconnect};
|
|
2
|
+
|
|
3
|
+
//# debugId=2B87FA8B1177915764756E2164756E21
|
|
4
|
+
//# sourceMappingURL=sortable.min.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["features/sortable.js"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// phlex/reactive/features/sortable — drag rows to reorder a list, or drag a\n// row in from another list, and send ONE declared action with the row and its\n// new position (issue #350). One of the feature modules the core imports on\n// demand (issue #275); in the default client it is part of the one file.\n//\n// The markup is the Ruby helpers' (Phlex::Reactive::Component::Sortable):\n//\n// data-reactive-sortable=\"move\" a list; the action a drop calls\n// -group / -handle / -name / -from / -params / -disabled / -busy / -messages\n// data-reactive-sortable-source=\"name\" a list whose rows are COPIED in\n// data-reactive-sortable-item='7' a row and its JSON-typed key\n//\n// and what it sets while a drag is on, for the app's CSS:\n//\n// data-reactive-sortable-active on the list being dragged from/over\n// data-reactive-sortable-dragging on the dragged row (or its clone)\n// data-reactive-sortable-placeholder the gap that marks the drop point\n//\n// Input is pointer events, not HTML5 drag and drop: mouse, pen and touch\n// behave the same, the dragged row is the real row under the pointer (no\n// ghost image), and the keyboard gets the same moves. A drag starts after a\n// 4 px move, or on touch after a 200 ms press, so taps, clicks and scrolling\n// still work.\n//\n// A drop moves the row in the DOM at once (a drag-in inserts nothing: the\n// target's action decides and its reply renders the row), then sends the\n// action through the root's own pipeline (core.proceed): the signed token,\n// the request queue, the busy state, the reply. The row goes back to where it\n// was when the request fails, is vetoed, or its reply does not render the\n// list. A reply that does render it (a morph) puts the rows in the server's\n// order: Idiomorph matches them by id.\n//\n// The geometry is every element's own getBoundingClientRect() — nothing is\n// cached across moves — so a test without layout stubs it per element.\n//\n// This module never imports the core: it reaches a controller through the\n// `core` handle (core.owns, core.proceed).\n\nconst LIST = \"[data-reactive-sortable]\"\nconst SOURCE = \"[data-reactive-sortable-source]\"\nconst ITEM = \"[data-reactive-sortable-item]\"\nconst ROOT = '[data-controller~=\"reactive\"]'\nconst FIELD = \"input,textarea,select,[contenteditable]\"\nconst THRESHOLD = 4 // px a press must move before it is a drag\nconst PRESS_MS = 200 // how long a touch is held before it picks a row up\nconst EDGE = 40 // px from an edge where auto-scroll starts\nconst SPEED = 12 // px auto-scroll moves per frame\n\n// controller -> the removals of what connect() added.\nconst wired = new WeakMap()\n// reactive root element -> its core handle: a drop into a list of ANOTHER root\n// (a drag-in, a move between roots of one group) sends through that root.\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\n\nexport function connect(controller, core) {\n const root = controller.element\n cores.set(root, core)\n // The live region is on the page, empty, before anything is announced: a\n // screen reader often ignores a region that arrives already holding words.\n liveRegion()\n const press = (event) => pointerDown(event, core)\n const key = (event) => keyDown(event, core)\n root.addEventListener(\"pointerdown\", press)\n root.addEventListener(\"keydown\", key)\n wired.set(controller, () => {\n root.removeEventListener(\"pointerdown\", press)\n root.removeEventListener(\"keydown\", key)\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 && rootOf(drag.origin) === controller.element) cancel()\n}\n\nconst attr = (el, name = \"\") => el?.getAttribute?.(`data-reactive-sortable${name}`)\nconst json = (value, fallback) => {\n try {\n return value == null ? fallback : JSON.parse(value)\n } catch {\n return fallback\n }\n}\nconst rootOf = (el) => el?.closest?.(ROOT)\nconst containerOf = (item) => item.parentElement?.closest(`${LIST},${SOURCE}`)\n// A list's rows: its items whose closest list is this one (a list nested in a\n// row keeps its own rows), minus the one being dragged.\nconst itemsOf = (list) =>\n [...list.querySelectorAll(ITEM)].filter((el) => el !== drag?.row && containerOf(el) === list)\nconst keyOf = (item) => json(attr(item, \"-item\"), null)\nconst disabled = (el) => attr(el, \"-disabled\") === \"true\"\nconst inside = (el, x, y) => {\n const rect = el.getBoundingClientRect()\n return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom\n}\n// The lists a row from `origin` can be dropped into: every enabled sortable of\n// its group whose root has a core, or (no group) the list itself.\nconst targetsFor = (origin) => {\n const group = attr(origin, \"-group\")\n if (!group) return origin.matches(LIST) ? [origin] : []\n return [...document.querySelectorAll(LIST)].filter(\n (list) => attr(list, \"-group\") === group && !disabled(list) && cores.has(rootOf(list)),\n )\n}\n\n// The row a gesture on `target` may pick up, or nothing: a row of a list this\n// root owns (a nested root's rows are its own, issue #15), neither of them\n// disabled, and no request of that root in flight.\nfunction grab(target, core) {\n const item = target?.closest?.(ITEM)\n const list = item && containerOf(item)\n if (drag || !list || !core.owns(list) || disabled(list) || disabled(item)) return\n if (rootOf(list)?.getAttribute(\"aria-busy\") === \"true\") return\n return { item, list }\n}\n\n// --- Pointer ----------------------------------------------------------------\nfunction pointerDown(event, core) {\n if (event.button > 0 || event.isPrimary === false) return\n const grabbed = grab(event.target, core)\n if (!grabbed) return\n const handle = attr(grabbed.list, \"-handle\")\n if (handle ? !grabbed.item.contains(event.target.closest(handle)) : event.target.closest(FIELD)) return\n\n const x0 = event.clientX\n const y0 = event.clientY\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 touch = event.pointerType === \"touch\"\n let ready = !touch\n const timer = touch && setTimeout(() => (ready = true), PRESS_MS)\n const move = (e) => {\n if (!mine(e)) 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 // A finger that moves before the press is up is scrolling.\n if (!ready) return end()\n start(grabbed, core, e)\n follow(e.clientX, e.clientY)\n }\n // While a touch drag is on, the page must not scroll under it.\n const hold = (e) => drag && e.cancelable && e.preventDefault()\n const up = (e) => {\n if (!mine(e)) return\n end()\n if (drag) suppressClick(), drop()\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 [\"touchmove\", hold, { passive: false }],\n ]\n for (const [type, fn, options] of on) document.addEventListener(type, fn, options)\n function end() {\n clearTimeout(timer)\n for (const [type, fn] of on) document.removeEventListener(type, fn)\n }\n}\n\n// The click a pointer drag ends with (over a button in the row) 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\n// Lift the row (a source's row: a clone of it) under the pointer, and leave\n// a placeholder of its height where it was.\nfunction start({ item, list }, core, event) {\n const rect = item.getBoundingClientRect()\n const source = list.matches(SOURCE)\n const row = source ? item.cloneNode(true) : item\n if (source) {\n // A clone must not connect a controller or duplicate an id.\n for (const el of [row, ...row.querySelectorAll(\"[id],[data-controller]\")]) {\n el.removeAttribute(\"id\")\n el.removeAttribute(\"data-controller\")\n }\n document.body.append(row)\n }\n const placeholder = document.createElement(item.tagName)\n placeholder.setAttribute(\"data-reactive-sortable-placeholder\", \"\")\n placeholder.setAttribute(\"aria-hidden\", \"true\")\n placeholder.style.height = `${rect.height}px`\n drag = begin({ core, item, row, origin: list, source, placeholder, style: row.getAttribute(\"style\") })\n drag.dx = event.clientX - rect.left\n drag.dy = event.clientY - rect.top\n drag.select = document.documentElement.style.userSelect\n document.documentElement.style.userSelect = \"none\"\n if (!source) item.before(placeholder)\n Object.assign(row.style, {\n position: \"fixed\",\n left: `${rect.left}px`,\n top: `${rect.top}px`,\n width: `${rect.width}px`,\n height: `${rect.height}px`,\n margin: \"0\",\n boxSizing: \"border-box\",\n zIndex: \"2147483647\",\n pointerEvents: \"none\",\n })\n}\n\n// The drag record both inputs share; marks the row and its list.\nfunction begin(record) {\n const { row, origin, source } = record\n record.index = itemsOf(origin).indexOf(record.item)\n record.target = source ? null : origin\n row.setAttribute(\"data-reactive-sortable-dragging\", \"\")\n activate(origin, true)\n return record\n}\n\nconst activate = (list, on) => list?.toggleAttribute(\"data-reactive-sortable-active\", on)\n\n// Put `el` at `index` among a list's rows (`items`, which leave `el` out):\n// before the row there, or after the last one, or into the empty list.\nfunction insertAt(list, items, index, el) {\n if (items[index]) items[index].before(el)\n else if (items.length) items.at(-1).after(el)\n else list.append(el)\n}\n\nfunction follow(x, y) {\n drag.x = x\n drag.y = y\n drag.row.style.left = `${x - drag.dx}px`\n drag.row.style.top = `${y - drag.dy}px`\n const list = targetsFor(drag.origin).find((el) => inside(el, x, y))\n if (list) place(list, x, y)\n // Off every list, a drag-in has nowhere to land; a move keeps its last spot.\n else if (drag.source) retarget(null), drag.placeholder.remove()\n scroll(x, y)\n}\n\nfunction retarget(list) {\n if (drag.target !== drag.origin) activate(drag.target, false)\n drag.target = list\n activate(list, true)\n}\n\n// The placeholder goes before the first row whose middle is past the pointer.\n// A list whose first two rows share a top is laid out across, not down.\nfunction place(list, x, y) {\n retarget(list)\n const items = itemsOf(list)\n const [a, b] = items.map((el) => el.getBoundingClientRect())\n const across = b && a.top === b.top && a.left !== b.left\n const index = items.findIndex((el) => {\n const rect = el.getBoundingClientRect()\n return across ? x < rect.left + rect.width / 2 : y < rect.top + rect.height / 2\n })\n insertAt(list, items, index < 0 ? items.length : index, drag.placeholder)\n}\n\n// Near the top or bottom edge of the list being dragged over (when it\n// scrolls) or of the window, scroll one step a frame until the pointer leaves\n// the edge.\nfunction scroll(x, y) {\n const list = drag.target ?? drag.origin\n const rect = list.getBoundingClientRect()\n const scrolls = list.scrollHeight > list.clientHeight\n drag.vy = scrolls ? (y < rect.top + EDGE ? -SPEED : y > rect.bottom - EDGE ? SPEED : 0) : 0\n drag.wy = y < EDGE ? -SPEED : y > window.innerHeight - EDGE ? SPEED : 0\n if ((drag.vy || drag.wy) && !drag.raf) {\n const step = () => {\n if (!drag || !(drag.vy || drag.wy)) return drag && (drag.raf = 0)\n ;(drag.target ?? drag.origin).scrollTop += drag.vy\n if (drag.wy) window.scrollBy(0, drag.wy)\n follow(drag.x, drag.y)\n drag.raf = requestAnimationFrame(step)\n }\n drag.raf = requestAnimationFrame(step)\n }\n}\n\n// --- Keyboard ----------------------------------------------------------------\n// A focused row: Space/Enter picks it up, the arrows move it (← / → into the\n// previous / next list of its group), Space/Enter drops it, Escape (or Tab)\n// puts it back. Each step is announced in the list's own words.\nfunction keyDown(event, core) {\n const key = event.key\n if (drag?.keyboard) {\n // (A nested root's row bubbles to every ancestor root: only the root that\n // picked the row up answers.)\n if (event.target !== drag.row || drag.core !== core) return\n const handled = {\n \" \": drop,\n Enter: drop,\n Escape: cancel,\n Tab: cancel,\n ArrowUp: () => step(-1),\n ArrowDown: () => step(1),\n ArrowLeft: () => hop(-1),\n ArrowRight: () => hop(1),\n }[key]\n if (!handled) return\n if (key !== \"Tab\") event.preventDefault()\n return handled()\n }\n if (key !== \" \" && key !== \"Enter\") return\n const grabbed = grab(event.target, core)\n if (!grabbed || grabbed.item !== event.target || grabbed.list.matches(SOURCE)) return\n event.preventDefault()\n drag = begin({ core, keyboard: true, item: grabbed.item, row: grabbed.item, origin: grabbed.list, style: grabbed.item.getAttribute(\"style\") })\n announce(\"picked\")\n}\n\nfunction step(by) {\n const items = itemsOf(drag.target)\n // (itemsOf leaves the dragged row out: it sits before items[index].)\n const to = position() + by\n // Past either end the row stays, and where it is is said again.\n if (to >= 0 && to <= items.length) insertAt(drag.target, items, to, drag.row)\n moved()\n}\n\nfunction hop(by) {\n const lists = targetsFor(drag.origin)\n const list = lists[lists.indexOf(drag.target) + by]\n if (list) {\n const index = position()\n retarget(list)\n insertAt(list, itemsOf(list), index, drag.row)\n }\n moved()\n}\n\nfunction moved() {\n drag.row.focus?.()\n announce(\"moved\")\n}\n\n// The dragged row's index in its target list (the drop's `position`).\nfunction position() {\n const items = itemsOf(drag.target)\n const row = drag.keyboard ? drag.row : drag.placeholder\n // (itemsOf leaves the dragged row out: count the rows before it.)\n return items.filter((el) => el.compareDocumentPosition(row) & 4).length\n}\n\nfunction liveRegion() {\n if (!live?.isConnected) {\n live = document.createElement(\"div\")\n live.setAttribute(\"aria-live\", \"polite\")\n live.setAttribute(\"data-reactive-sortable-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(name) {\n const list = drag.target ?? drag.origin\n const words = (json(attr(list, \"-messages\"), {})[name] ?? \"\")\n .replaceAll(\"{position}\", position() + 1)\n .replaceAll(\"{count}\", itemsOf(list).length + 1)\n const region = liveRegion()\n region.textContent = \"\"\n cancelAnimationFrame(speaking)\n speaking = requestAnimationFrame(() => (region.textContent = words))\n}\n\n// --- Drop / cancel -----------------------------------------------------------\n// Settle the row, then send the target's action: a move that changed nothing\n// sends nothing. Returns before the reply; the row goes back if it fails.\nfunction drop() {\n const record = drag\n const { target, origin, source, row, placeholder } = record\n const landed = target && (source ? placeholder.isConnected : true)\n const index = landed ? position() : -1\n const after = landed ? keyOf(itemsOf(target)[index - 1]) : null\n if (record.keyboard) announce(\"dropped\")\n if (!source && placeholder) placeholder.replaceWith(row)\n settle(record)\n if (!landed || (target === origin && index === record.index)) return\n\n const from = target === origin ? \"self\" : source ? attr(origin, \"-source\") : (attr(origin, \"-name\") ?? \"list\")\n const action = json(attr(target, \"-from\"), {})[from] ?? attr(target)\n const params = { ...json(attr(target, \"-params\"), {}), item: keyOf(record.item), position: index, after, from }\n // The list and its ancestors: a reply stream that replaces or updates one\n // of them (by id, or by a `targets:` selector) rendered the list (read now —\n // a replace detaches it).\n const chain = []\n for (let el = target; el; el = el.parentElement) chain.push(el)\n const sent = cores.get(rootOf(target))?.proceed(target, action, JSON.stringify(params), json(attr(target, \"-busy\")))\n if (source) return\n Promise.resolve(sent).then((html) => renders(html, chain) || restore(record))\n}\n\nfunction renders(html, chain) {\n if (!html) return false\n const template = document.createElement(\"template\")\n template.innerHTML = html\n return [...template.content.querySelectorAll(\"turbo-stream\")].some(\n (stream) => /^(replace|update)$/.test(stream.getAttribute(\"action\")) && hits(stream, chain),\n )\n}\n\nfunction hits(stream, chain) {\n const target = stream.getAttribute(\"target\")\n if (target) return chain.some((el) => el.id === target)\n const selector = stream.getAttribute(\"targets\")\n try {\n return Boolean(selector) && chain.some((el) => el.matches(selector))\n } catch {\n return false // an invalid selector renders nothing\n }\n}\n\nfunction cancel() {\n const record = drag\n if (record.keyboard) announce(\"cancelled\")\n record.placeholder?.remove()\n settle(record)\n restore(record)\n}\n\n// Put a moved row back at its original index in its original list.\nfunction restore({ row, origin, index, source }) {\n if (source || !row.isConnected) return\n insertAt(\n origin,\n itemsOf(origin).filter((el) => el !== row),\n index,\n row,\n )\n if (row === document.activeElement) row.focus?.()\n}\n\n// Undo everything the drag set: the row's style and marker, the lists'\n// markers, the clone, the placeholder, the frame loop, the text selection.\nfunction settle(record) {\n const { row, origin, target, source, style, placeholder } = record\n drag = null\n if (record.raf) cancelAnimationFrame(record.raf)\n row.removeAttribute(\"data-reactive-sortable-dragging\")\n if (source) row.remove()\n else if (!record.keyboard) style == null ? row.removeAttribute(\"style\") : row.setAttribute(\"style\", style)\n placeholder?.remove()\n activate(origin, false)\n activate(target, false)\n if (!record.keyboard) document.documentElement.style.userSelect = record.select ?? \"\"\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2B87FA8B1177915764756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -32,6 +32,7 @@ import * as defer from "phlex/reactive/features/defer"
|
|
|
32
32
|
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
|
+
import * as sortable from "phlex/reactive/features/sortable"
|
|
35
36
|
import * as effects from "phlex/reactive/features/effects"
|
|
36
37
|
import * as hints from "phlex/reactive/features/hints"
|
|
37
38
|
import * as devtools from "phlex/reactive/features/devtools"
|
|
@@ -63,6 +64,7 @@ if (__SPLIT__) {
|
|
|
63
64
|
checkGroup: bindings.checkGroup,
|
|
64
65
|
})
|
|
65
66
|
registerReactiveFeature("compute", { connect: compute.connect, disconnect: compute.disconnect, seed: compute.seed, recompute: compute.recompute })
|
|
67
|
+
registerReactiveFeature("sortable", { connect: sortable.connect, disconnect: sortable.disconnect })
|
|
66
68
|
registerReactiveFeature("effects", { wrap: effects.wrap, sweep: effects.sweep })
|
|
67
69
|
registerReactiveFeature("hints", { optimistic: hints.optimistic, busy: hints.busy, resurrection: hints.resurrection })
|
|
68
70
|
registerReactiveFeature("devtools", {
|