1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["input"] 5 6 connect() { 7 this.element.addEventListener("click", this.click.bind(this)) 8 9 // Capture drag events on the whole page 10 this.boundPageDragover = this.pageDragover.bind(this) 11 this.boundPageDragleave = this.pageDragleave.bind(this) 12 this.boundPageDrop = this.pageDrop.bind(this) 13 14 document.addEventListener("dragover", this.boundPageDragover) 15 document.addEventListener("dragleave", this.boundPageDragleave) 16 document.addEventListener("drop", this.boundPageDrop) 17 18 // Prevent default browser behavior for drag/drop 19 document.addEventListener("dragenter", this.preventDefault) 20 } 21 22 disconnect() { 23 document.removeEventListener("dragover", this.boundPageDragover) 24 document.removeEventListener("dragleave", this.boundPageDragleave) 25 document.removeEventListener("drop", this.boundPageDrop) 26 document.removeEventListener("dragenter", this.preventDefault) 27 } 28 29 preventDefault(event) { 30 event.preventDefault() 31 } 32 33 click(event) { 34 if (event.target === this.inputTarget) return 35 this.inputTarget.click() 36 } 37 38 pageDragover(event) { 39 event.preventDefault() 40 event.stopPropagation() 41 this.element.classList.add("dragover") 42 } 43 44 pageDragleave(event) { 45 event.preventDefault() 46 // Only remove dragover if leaving the window 47 if (event.relatedTarget === null || !document.body.contains(event.relatedTarget)) { 48 this.element.classList.remove("dragover") 49 } 50 } 51 52 pageDrop(event) { 53 event.preventDefault() 54 event.stopPropagation() 55 this.element.classList.remove("dragover") 56 57 const files = event.dataTransfer.files 58 if (files.length > 0) { 59 // Check if file type is valid 60 const file = files[0] 61 const validTypes = ['.csv', '.pdf', '.xlsx', '.xls'] 62 const fileName = file.name.toLowerCase() 63 const isValid = validTypes.some(ext => fileName.endsWith(ext)) 64 65 if (isValid) { 66 this.inputTarget.files = files 67 this.showFileSelected(file) 68 } else { 69 this.showError("Please drop a PDF, CSV, or Excel file") 70 } 71 } 72 } 73 74 // Legacy handlers for direct drop zone events 75 dragover(event) { 76 event.preventDefault() 77 this.element.classList.add("dragover") 78 } 79 80 dragleave(event) { 81 event.preventDefault() 82 this.element.classList.remove("dragover") 83 } 84 85 drop(event) { 86 event.preventDefault() 87 this.element.classList.remove("dragover") 88 89 const files = event.dataTransfer.files 90 if (files.length > 0) { 91 this.inputTarget.files = files 92 this.showFileSelected(files[0]) 93 } 94 } 95 96 fileSelected(event) { 97 const files = event.target.files 98 if (files.length > 0) { 99 this.showFileSelected(files[0]) 100 } 101 } 102 103 showFileSelected(file) { 104 this.element.classList.add("has-file") 105 this.element.classList.remove("has-error") 106 107 // Update content for new drop zone layout 108 const content = this.element.querySelector(".drop-zone-content") 109 if (content) { 110 const icon = content.querySelector(".drop-zone-icon i") 111 const title = content.querySelector(".drop-zone-title") 112 const subtitle = content.querySelector(".drop-zone-subtitle") 113 const formats = content.querySelector(".drop-zone-formats") 114 115 if (icon) { 116 icon.className = "bi bi-file-earmark-check" 117 } 118 if (title) { 119 title.textContent = file.name 120 } 121 if (subtitle) { 122 subtitle.textContent = this.formatFileSize(file.size) 123 } 124 if (formats) { 125 formats.innerHTML = '<span class="text-success"><i class="bi bi-check-circle me-1"></i>Ready to upload</span>' 126 } 127 } 128 129 // Fallback for old layout 130 const textEl = this.element.querySelector(".text-slate-500") 131 if (textEl && !content) { 132 textEl.textContent = file.name 133 } 134 } 135 136 showError(message) { 137 this.element.classList.add("has-error") 138 this.element.classList.remove("has-file") 139 140 const content = this.element.querySelector(".drop-zone-content") 141 if (content) { 142 const formats = content.querySelector(".drop-zone-formats") 143 if (formats) { 144 formats.innerHTML = `<span class="text-danger"><i class="bi bi-exclamation-circle me-1"></i>${message}</span>` 145 } 146 } 147 148 // Reset after 3 seconds 149 setTimeout(() => { 150 this.element.classList.remove("has-error") 151 if (content) { 152 const formats = content.querySelector(".drop-zone-formats") 153 if (formats) { 154 formats.textContent = "Supports PDF, CSV, Excel files" 155 } 156 } 157 }, 3000) 158 } 159 160 formatFileSize(bytes) { 161 if (bytes === 0) return '0 Bytes'
162 const k = 1024 163 const sizes = ['Bytes', 'KB', 'MB', 'GB'] 164 const i = Math.floor(Math.log(bytes) / Math.log(k)) 165 return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] 166 } 167};
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.