phlex-reactive 0.14.3 → 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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +67 -4
  4. data/app/javascript/phlex/reactive/core.js +7 -1
  5. data/app/javascript/phlex/reactive/core.min.js +6 -2
  6. data/app/javascript/phlex/reactive/core.min.js.map +4 -4
  7. data/app/javascript/phlex/reactive/features/bindings.js +96 -0
  8. data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
  9. data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
  10. data/app/javascript/phlex/reactive/features/form.js +12 -5
  11. data/app/javascript/phlex/reactive/features/form.min.js +2 -2
  12. data/app/javascript/phlex/reactive/features/form.min.js.map +3 -3
  13. data/app/javascript/phlex/reactive/features/persist.js +12 -3
  14. data/app/javascript/phlex/reactive/features/persist.min.js +2 -2
  15. data/app/javascript/phlex/reactive/features/persist.min.js.map +3 -3
  16. data/app/javascript/phlex/reactive/features/sortable.js +463 -0
  17. data/app/javascript/phlex/reactive/features/sortable.min.js +4 -0
  18. data/app/javascript/phlex/reactive/features/sortable.min.js.map +10 -0
  19. data/app/javascript/phlex/reactive/reactive_controller.js +2 -0
  20. data/app/javascript/phlex/reactive/reactive_controller.min.js +6 -2
  21. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +9 -8
  22. data/app/javascript/phlex/reactive/runtime.js +117 -10
  23. data/lib/phlex/reactive/component/editable.rb +78 -0
  24. data/lib/phlex/reactive/component/helpers.rb +2 -0
  25. data/lib/phlex/reactive/component/sortable.rb +212 -0
  26. data/lib/phlex/reactive/engine.rb +4 -4
  27. data/lib/phlex/reactive/version.rb +1 -1
  28. 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", {