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.