1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["item", "list", "moveDown", "moveUp", "position", "status"] 5 6 connect() { 7 this.originalOrder = this.itemTargets.map((item) => item.dataset.benefitId) 8 this.nativeDrag = null 9 this.pointerDrag = null 10 this.previousBodyUserSelect = null 11 this.updateControls() 12 } 13 14 disconnect() { 15 this.finishNativeDrag() 16 this.finishPointerDrag() 17 } 18 19 moveUp(event) { 20 const item = event.currentTarget.closest("[data-benefit-reorder-target='item']") 21 const previousItem = item?.previousElementSibling 22 if (!item || !previousItem) return 23 24 previousItem.before(item) 25 this.updateControls(item) 26 this.focusMoveControl(item, event.currentTarget) 27 } 28 29 moveDown(event) { 30 const item = event.currentTarget.closest("[data-benefit-reorder-target='item']") 31 const nextItem = item?.nextElementSibling 32 if (!item || !nextItem) return 33 34 nextItem.after(item) 35 this.updateControls(item) 36 this.focusMoveControl(item, event.currentTarget) 37 } 38 39 focusMoveControl(item, control) { 40 if (!control.disabled) { 41 control.focus() 42 return 43 } 44 item.querySelector("[data-benefit-reorder-target='moveUp']:not([disabled]), [data-benefit-reorder-target='moveDown']:not([disabled])")?.focus() 45 } 46 47 noop(event) { 48 event.preventDefault() 49 } 50 51 dragStart(event) { 52 const item = event.currentTarget.closest("[data-benefit-reorder-target='item']") 53 if (!item || this.itemTargets.length <= 1) { 54 event.preventDefault() 55 return 56 } 57 58 this.finishPointerDrag() 59 this.nativeDrag = { item, moved: false } 60 this.showDragging(item) 61 62 event.dataTransfer.effectAllowed = "move" 63 event.dataTransfer.setData("text/plain", item.dataset.benefitId) 64 } 65 66 dragOver(event) { 67 if (!this.nativeDrag) return 68 69 event.preventDefault() 70 event.dataTransfer.dropEffect = "move" 71 72 const over = event.target.closest("[data-benefit-reorder-target='item']") 73 if (this.moveDraggedItem(this.nativeDrag, over, event.clientY)) this.updateControls() 74 } 75 76 drop(event) { 77 if (!this.nativeDrag) return 78 79 event.preventDefault() 80 this.finishNativeDrag() 81 } 82 83 dragEnd() { 84 this.finishNativeDrag() 85 } 86 87 startPointerDrag(event) { 88 if (event.pointerType === "mouse") return 89 if (event.button !== undefined && event.button !== 0) return 90 91 const handle = event.currentTarget 92 const item = handle.closest("[data-benefit-reorder-target='item']") 93 if (!item || this.itemTargets.length <= 1) return 94 95 event.preventDefault() 96 this.finishNativeDrag() 97 this.finishPointerDrag() 98 99 this.pointerDrag = { 100 pointerId: event.pointerId, 101 handle, 102 item, 103 moved: false 104 } 105 106 if (handle.setPointerCapture) handle.setPointerCapture(event.pointerId) 107 this.showDragging(item) 108 this.previousBodyUserSelect = document.body.style.userSelect 109 document.body.style.userSelect = "none" 110 } 111 112 dragPointer(event) { 113 if (!this.pointerDrag || event.pointerId !== this.pointerDrag.pointerId) return 114 115 event.preventDefault() 116 const over = document.elementFromPoint(event.clientX, event.clientY)?.closest?.( 117 "[data-benefit-reorder-target='item']" 118 ) 119 if (this.moveDraggedItem(this.pointerDrag, over, event.clientY)) this.updateControls() 120 } 121 122 endPointerDrag(event) { 123 if (!this.pointerDrag || event.pointerId !== this.pointerDrag.pointerId) return 124 125 event.preventDefault() 126 this.finishPointerDrag() 127 } 128 129 reset() { 130 if (!this.hasListTarget) return 131 132 this.finishNativeDrag() 133 this.finishPointerDrag() 134 135 const itemsById = new Map(this.itemTargets.map((item) => [item.dataset.benefitId, item])) 136 this.originalOrder.forEach((id) => { 137 const item = itemsById.get(id) 138 if (item) this.listTarget.append(item) 139 }) 140 this.updateControls() 141 if (this.hasStatusTarget) this.statusTarget.textContent = "" 142 } 143 144 finishNativeDrag() { 145 if (!this.nativeDrag) return 146 147 const { item, moved } = this.nativeDrag 148 this.hideDragging(item) 149 this.nativeDrag = null 150 151 if (moved) this.updateControls(item) 152 } 153 154 finishPointerDrag() { 155 if (!this.pointerDrag) return 156 157 const { handle, pointerId, item, moved } = this.pointerDrag 158 159 if (handle?.releasePointerCapture && handle.hasPointerCapture?.(pointerId)) { 160 handle.releasePointerCapture(pointerId) 161 } 162 163 this.hideDragging(item) 164 165 if (this.previousBodyUserSelect === null) { 166 document.body.style.removeProperty("user-select") 167 } else { 168 document.body.style.userSelect = this.previousBodyUserSelect 169 } 170 this.previousBodyUserSelect = null 171 this.pointerDrag = null 172 173 if (moved) this.updateControls(item) 174 } 175 176 moveDraggedItem(drag, over, clientY) { 177 const item = drag.item 178 if (!over || over === item || over.parentElement !== this.listTarget) return false 179 180 const rect = over.getBoundingClientRect() 181 const insertBefore = clientY < rect.top + rect.height / 2 182 183 if (insertBefore) { 184 if (item === over.previousElementSibling) return false 185 over.before(item) 186 } else { 187 if (item === over.nextElementSibling) return false 188 over.after(item) 189 } 190 191 drag.moved = true 192 return true 193 } 194 195 showDragging(item) { 196 item.classList.add("opacity-80", "ring-2", "ring-amber-500/20") 197 } 198 199 hideDragging(item) { 200 item.classList.remove("opacity-80", "ring-2", "ring-amber-500/20") 201 } 202 203 updateControls(movedItem = null) { 204 const items = this.itemTargets 205
206 items.forEach((item, index) => { 207 const position = item.querySelector("[data-benefit-reorder-target='position']") 208 const moveUp = item.querySelector("[data-benefit-reorder-target='moveUp']") 209 const moveDown = item.querySelector("[data-benefit-reorder-target='moveDown']") 210 211 if (position) position.textContent = index + 1 212 if (moveUp) moveUp.disabled = index === 0 213 if (moveDown) moveDown.disabled = index === items.length - 1 214 }) 215 216 if (movedItem && this.hasStatusTarget) { 217 const position = items.indexOf(movedItem) + 1 218 this.statusTarget.textContent = `${movedItem.dataset.benefitName} moved to position ${position} of ${items.length}.` 219 } 220 } 221}
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.