1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["fallback", "enhanced", "list", "item", "answer", "position", "handle", "picker", "instructions", "blank", "skip", "skippedMessage", "status"] 5 static values = { skipped: Boolean } 6 7 connect() { 8 this.fallbackTarget.hidden = true 9 this.fallbackTarget.querySelectorAll("select").forEach((select) => { select.disabled = true }) 10 this.enhancedTarget.hidden = false 11 this.closePicker() 12 this.update() 13 } 14 15 disconnect() { 16 this.cancelPointerDrag() 17 this.closePicker() 18 } 19 20 reorderKey(event) { 21 if (this.previewDisabled || !["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return 22 event.preventDefault() 23 this.closePicker() 24 const item = this.itemFor(event.currentTarget) 25 const index = this.itemTargets.indexOf(item) 26 const position = { ArrowUp: index - 1, ArrowDown: index + 1, Home: 0, End: this.itemTargets.length - 1 }[event.key] 27 this.moveTo(item, position) 28 } 29 30 togglePicker(event) { 31 if (this.previewDisabled || (event.detail > 0 && event.timeStamp - this.lastDragTime < 300)) return 32 const item = this.itemFor(event.currentTarget) 33 const picker = item.querySelector("[data-ranking-input-target='picker']") 34 const wasOpen = !picker.hidden 35 this.closePicker() 36 if (wasOpen) return 37 38 picker.hidden = false 39 event.currentTarget.setAttribute("aria-expanded", "true") 40 const select = picker.querySelector("select") 41 select.value = this.itemTargets.indexOf(item) 42 select.focus() 43 } 44 45 choosePosition(event) { 46 if (this.previewDisabled) return 47 const item = this.itemFor(event.currentTarget) 48 this.closePicker() 49 this.moveTo(item, Number(event.currentTarget.value)) 50 } 51 52 closePicker() { 53 this.pickerTargets.forEach((picker) => { picker.hidden = true }) 54 this.handleTargets.forEach((handle) => { handle.setAttribute("aria-expanded", "false") }) 55 } 56 57 closeOutside(event) { 58 const openPicker = this.pickerTargets.find((picker) => !picker.hidden) 59 if (openPicker && !this.itemFor(openPicker).contains(event.target)) this.closePicker() 60 } 61 62 focusOut(event) { 63 const openPicker = this.pickerTargets.find((picker) => !picker.hidden) 64 if (openPicker && !this.itemFor(openPicker).contains(event.relatedTarget)) this.closePicker() 65 } 66 67 escape(event) { 68 const openPicker = this.pickerTargets.find((picker) => !picker.hidden) 69 if (!openPicker && !this.pointerDrag) return 70 event.preventDefault() 71 this.cancelPointerDrag() 72 this.closePicker() 73 if (openPicker) this.focusHandle(this.itemFor(openPicker)) 74 } 75 76 startPointerDrag(event) { 77 if (this.previewDisabled || event.button !== 0 || event.isPrimary === false) return 78 this.cancelPointerDrag() 79 this.pointerDrag = { 80 pointerId: event.pointerId, 81 item: this.itemFor(event.currentTarget), 82 originalOrder: this.itemTargets, 83 startY: event.clientY, 84 moved: false 85 } 86 } 87 88 dragPointer(event) { 89 const drag = this.pointerDrag 90 if (!drag || event.pointerId !== drag.pointerId) return 91 if (!drag.moved && Math.abs(event.clientY - drag.startY) < 5) return 92 event.preventDefault() 93 if (!drag.moved) { 94 drag.moved = true 95 this.closePicker() 96 this.element.setPointerCapture(event.pointerId) 97 drag.item.classList.add("opacity-50") 98 } 99 100 const others = this.itemTargets.filter((item) => item !== drag.item) 101 const next = others.find((item) => { 102 const rect = item.getBoundingClientRect() 103 return event.clientY < rect.top + rect.height / 2 104 }) 105 if (next) next.before(drag.item) 106 else this.listTarget.append(drag.item) 107 this.update() 108 } 109 110 endPointerDrag(event) { 111 if (!this.pointerDrag || event.pointerId !== this.pointerDrag.pointerId) return 112 const { item, moved } = this.pointerDrag 113 this.finishPointerDrag() 114 if (moved) { 115 this.lastDragTime = event.timeStamp 116 this.moved(item) 117 } 118 } 119 120 cancelPointerDrag() { 121 if (!this.pointerDrag) return 122 this.pointerDrag.originalOrder.forEach((item) => this.listTarget.append(item)) 123 this.finishPointerDrag() 124 this.update() 125 } 126 127 finishPointerDrag() { 128 const { pointerId, item } = this.pointerDrag 129 this.pointerDrag = null 130 if (this.element.hasPointerCapture(pointerId)) this.element.releasePointerCapture(pointerId) 131 item.classList.remove("opacity-50") 132 } 133 134 moveTo(item, position) { 135 const items = this.itemTargets 136 const destination = items[Math.max(0, Math.min(position, items.length - 1))] 137 if (items.indexOf(destination) < items.indexOf(item)) destination.before(item) 138 else if (destination !== item) destination.after(item) 139 this.moved(item) 140 } 141 142 moved(item) { 143 this.update() 144 const position = this.itemTargets.indexOf(item) + 1 145 this.statusTarget.textContent = `${item.dataset.label} is in position ${position} of ${this.itemTargets.length}.` 146 this.focusHandle(item) 147 } 148 149 toggleSkipped() { 150 if (this.previewDisabled) return 151 this.closePicker() 152 this.skippedValue = !this.skippedValue 153 this.update() 154 this.statusTarget.textContent = this.skippedValue ? "Question skipped." : "The displayed order will be submitted." 155 } 156 157 update() {
158 this.answerTargets.forEach((input) => { input.disabled = this.skippedValue || this.previewDisabled }) 159 this.listTarget.hidden = this.skippedValue 160 this.instructionsTarget.hidden = this.skippedValue 161 this.itemTargets.forEach((item, index) => { 162 item.querySelector("[data-ranking-input-target='position']").textContent = index + 1 163 }) 164 if (this.hasSkipTarget) { 165 this.blankTarget.disabled = !this.skippedValue || this.previewDisabled 166 this.skipTarget.textContent = this.skippedValue ? "Add ranking" : "Skip this question" 167 this.skippedMessageTarget.hidden = !this.skippedValue 168 } 169 } 170 171 itemFor(element) { 172 return element.closest("[data-ranking-input-target='item']") 173 } 174 175 focusHandle(item) { 176 item.querySelector("[data-ranking-input-target='handle']").focus() 177 } 178 179 get previewDisabled() { 180 return !!this.element.closest("fieldset[disabled]") 181 } 182}
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.