PageSourceSearch

https://hjarni.com/assets/controllers/sortable_controller-7a2238ae.js

js hjarni.com collected 2026-09-24 11:52:33 UTC 2,911 bytes, 106 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Native HTML drag-and-drop for reordering containers (folders).
4// Uses a drag threshold to avoid interfering with normal link clicks.
5export default class extends Controller {
6  static targets = ["item"]
7  static values = { url: String, parentId: String }
8
9  connect() {
10    this.dragItem = null
11    this.originalOrder = []
12  }
13
14  itemTargetConnected(el) {
15    el.setAttribute("draggable", "true")
16    el.addEventListener("dragstart", this.onDragStart)
17    el.addEventListener("dragend", this.onDragEnd)
18    el.addEventListener("dragover", this.onDragOver)
19    el.addEventListener("drop", this.onDrop)
20  }
21
22  itemTargetDisconnected(el) {
23    el.removeEventListener("dragstart", this.onDragStart)
24    el.removeEventListener("dragend", this.onDragEnd)
25    el.removeEventListener("dragover", this.onDragOver)
26    el.removeEventListener("drop", this.onDrop)
27  }
28
29  onDragStart = (e) => {
30    this.dragItem = e.currentTarget
31    // Snapshot original order so we can revert on failure
32    this.originalOrder = this.itemTargets.map(el => el.dataset.sortableId)
33    this.dragItem.classList.add("opacity-50")
34    e.dataTransfer.effectAllowed = "move"
35    e.dataTransfer.setData("text/plain", "")
36  }
37
38  onDragEnd = () => {
39    if (this.dragItem) {
40      this.dragItem.classList.remove("opacity-50")
41    }
42    this.dragItem = null
43  }
44
45  onDragOver = (e) => {
46    e.preventDefault()
47    e.stopPropagation()
48    e.dataTransfer.dropEffect = "move"
49
50    const target = e.currentTarget
51    if (!this.dragItem || target === this.dragItem) return
52
53    const rect = target.getBoundingClientRect()
54    const midY = rect.top + rect.height / 2
55
56    if (e.clientY < midY) {
57      target.parentNode.insertBefore(this.dragItem, target)
58    } else {
59      target.parentNode.insertBefore(this.dragItem, target.nextSibling)
60    }
61  }
62
63  onDrop = (e) => {
64    e.preventDefault()
65    e.stopPropagation()
66    this.save()
67  }
68
69  save() {
70    const orderedIds = this.itemTargets.map(el => parseInt(el.dataset.sortableId))
71    const csrfToken = document.querySelector("meta[name='csrf-token']")?.content
72
73    const body = { ordered_ids: orderedIds }
74    if (this.parentIdValue) {
75      body.parent_id = this.parentIdValue
76    }
77
78    fetch(this.urlValue, {
79      method: "PATCH",
80      headers: {
81        "Content-Type": "application/json",
82        "X-CSRF-Token": csrfToken,
83        "Accept": "application/json"
84      },
85      body: JSON.stringify(body)
86    }).then(response => {
87      if (!response.ok) {
88        this.revertOrder()
89      }
90    }).catch(() => {
91      this.revertOrder()
92    })
93  }
94
95  revertOrder() {
96    const parent = this.element
97    const itemsById = Object.fromEntries(
98      this.itemTargets.map(el => [el.dataset.sortableId, el])
99    )
100    for (const id of this.originalOrder) {
101      if (itemsById[id]) {
102        parent.appendChild(itemsById[id])
103      }
104    }
105  }
106}

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.