PageSourceSearch

https://bikeindex.org/assets/controllers/sortable_controller-ce4b5…81baac707d9e8e01476da3ca9b008a407432b354.js

js bikeindex.org collected 2026-09-24 08:54:43 UTC 3,734 bytes, 104 lines download raw bytes

1import { Controller } from '@hotwired/stimulus'
2
3// Connects to data-controller='sortable'
4// Pointer drag-to-reorder of [data-sortable-target=item] rows, grabbed by their
5// [data-sortable-target=handle] grip. A thin bar shows where the row will land; on release
6// the row is moved there and (when it has data-url) its new position is PATCHed.
7// Pointer events -- not native HTML5 draggable -- so a rich-text editor inside a row can't
8// swallow the drag and there is no native snap-back animation. Subclasses override reorder().
9export default class extends Controller {
10  static targets = ['item', 'handle']
11
12  connect () {
13    this.draggingItem = null
14    this.dropTarget = null
15    this.dropAfter = false
16    // Slid into the target gap during a drag; pointer-events:none so it never blocks hit-testing.
17    this.indicator = document.createElement('div')
18    this.indicator.setAttribute('aria-hidden', 'true')
19    this.indicator.style.cssText = 'height:2px;border-radius:9999px;background:#2563eb;pointer-events:none;'
20  }
21
22  disconnect () {
23    this.#stopTracking()
24  }
25
26  handleTargetConnected (handle) {
27    handle.addEventListener('pointerdown', this.#onPointerDown)
28  }
29
30  #onPointerDown = (event) => {
31    if (event.button !== 0) return
32    const item = event.target.closest(this.#itemSelector)
33    if (!item) return
34    event.preventDefault()
35    this.draggingItem = item
36    this.dropTarget = null
37    item.classList.add('tw:opacity-50')
38    document.addEventListener('pointermove', this.#onPointerMove)
39    document.addEventListener('pointerup', this.#onPointerUp, { once: true })
40    document.addEventListener('pointercancel', this.#onPointerCancel, { once: true })
41  }
42
43  #onPointerMove = (event) => {
44    const item = this.itemTargets.find((row) => {
45      const box = row.getBoundingClientRect()
46      return event.clientY >= box.top && event.clientY <= box.bottom
47    })
48    if (!item) return
49    this.dropTarget = item
50    this.dropAfter = this.#after(item, event.clientY)
51    item.parentNode.insertBefore(this.indicator, this.dropAfter ? item.nextSibling : item)
52  }
53
54  #onPointerUp = () => {
55    this.#stopTracking()
56    this.draggingItem.classList.remove('tw:opacity-50')
57    if (this.dropTarget && this.dropTarget !== this.draggingItem) {
58      this.reorder(this.dropTarget, this.dropAfter)
59    }
60    this.draggingItem = null
61    this.dropTarget = null
62  }
63
64  // The browser can hijack a touch gesture (scroll/zoom) and fire pointercancel; abort cleanly.
65  #onPointerCancel = () => {
66    this.#stopTracking()
67    this.draggingItem?.classList.remove('tw:opacity-50')
68    this.draggingItem = null
69    this.dropTarget = null
70  }
71
72  #stopTracking () {
73    document.removeEventListener('pointermove', this.#onPointerMove)
74    document.removeEventListener('pointerup', this.#onPointerUp)
75    document.removeEventListener('pointercancel', this.#onPointerCancel)
76    this.indicator.remove()
77  }
78
79  // Default: move the dragged row next to the target, then persist its new position.
80  reorder (target, after) {
81    target.parentNode.insertBefore(this.draggingItem, after ? target.nextSibling : target)
82    this.persist(this.draggingItem)
83  }
84
85  persist (item) {
86    if (!item.dataset.url) return
87    const position = this.itemTargets.indexOf(item)
88    const token = document.querySelector('meta[name="csrf-token"]')?.content
89    fetch(item.dataset.url, {
90      method: 'PATCH',
91      headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
92      body: JSON.stringify({ position })
93    })
94  }
95
96  #after (item, clientY) {
97    const box = item.getBoundingClientRect()
98    return clientY > box.top + box.height / 2
99  }
100
101  get #itemSelector () {
102    return `[data-${this.identifier}-target="item"]`
103  }
104};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.