1/** 2 * Passive Drop Target â turns any content area into a drag-to-open trigger. 3 * 4 * Usage: 5 * import { initPassiveDrop } from '../utils/passive-drop.js'; 6 * 7 * const destroy = initPassiveDrop(containerEl, (files) => { 8 * openUploadModal(); 9 * handleFilesSelected(files); 10 * }, { label: 'Drop to upload photos', accept: ['image/', 'video/'] }); 11 * 12 * // Cleanup on page destroy: 13 * destroy(); 14 * 15 * Options: 16 * label {string} Overlay caption. Default: 'Drop files to upload' 17 * accept {string[]} MIME prefixes to filter. Empty = accept all. Default: [] 18 * icon {string} Lucide icon name for the overlay card. Default: 'upload-cloud' 19 */ 20 21export function initPassiveDrop(container, onDrop, options = {}) { 22 if (!container || typeof onDrop !== 'function') return () => {}; 23 24 const { 25 label = 'Drop files to upload', 26 accept = [], 27 icon = 'upload-cloud', 28 } = options; 29 30 // ââ Overlay element ââââââââââââââââââââââââââââââââââââââââââ 31 const overlay = document.createElement('div'); 32 overlay.className = 'passive-drop-overlay'; 33 overlay.setAttribute('aria-hidden', 'true'); 34 overlay.innerHTML = ` 35 <div class="passive-drop-card"> 36 <i data-lucide="${icon}" class="passive-drop-icon"></i> 37 <span class="passive-drop-label">${label}</span> 38 </div>`; 39 40 // Position relative needed on container 41 const prevPosition = container.style.position; 42 if (!prevPosition || prevPosition === 'static') { 43 container.style.position = 'relative'; 44 } 45 46 // ââ Drag counter (handles child element enter/leave) âââââââââ 47 let dragDepth = 0; 48 49 function isAccepted(dataTransfer) { 50 if (!accept.length) return true; 51 // Check both items (if available) and types 52 if (dataTransfer.items && dataTransfer.items.length) { 53 return Array.from(dataTransfer.items).some(item => 54 accept.some(mime => item.type.startsWith(mime)) 55 ); 56 } 57 // Fallback: types array only gives "Files" â allow it and filter on drop 58 return Array.from(dataTransfer.types).includes('Files'); 59 } 60 61 function showOverlay() { 62 if (!overlay.parentElement) { 63 container.appendChild(overlay); 64 if (typeof lucide !== 'undefined') lucide.createIcons({ root: overlay }); 65 } 66 overlay.classList.add('active'); 67 } 68 69 function hideOverlay() { 70 overlay.classList.remove('active'); 71 } 72 73 // ââ Handlers ââââââââââââââââââââââââââââââââââââââââââââââââ 74 function onDragEnter(e) { 75 e.preventDefault(); 76 if (e.dataTransfer && !isAccepted(e.dataTransfer)) return; 77 dragDepth++; 78 if (dragDepth === 1) showOverlay(); 79 } 80 81 function onDragOver(e) { 82 e.preventDefault(); 83 if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; 84 } 85 86 function onDragLeave(e) { 87 dragDepth--; 88 if (dragDepth <= 0) { 89 dragDepth = 0; 90 hideOverlay(); 91 } 92 } 93 94 function onDropHandler(e) { 95 e.preventDefault(); 96 dragDepth = 0; 97 hideOverlay(); 98 99 const dt = e.dataTransfer; 100 if (!dt || !dt.files || dt.files.length === 0) return; 101 102 let files = Array.from(dt.files); 103 104 // Filter by accepted MIME types if specified 105 if (accept.length) { 106 files = files.filter(f => accept.some(mime => f.type.startsWith(mime))); 107 } 108 109 if (files.length === 0) { 110 window.showToast?.('Unsupported files', 'warning', { 111 description: 'Please drop image or video files', 112 }); 113 return; 114 } 115 116 onDrop(files); 117 } 118 119 container.addEventListener('dragenter', onDragEnter); 120 container.addEventListener('dragover', onDragOver); 121 container.addEventListener('dragleave', onDragLeave); 122 container.addEventListener('drop', onDropHandler); 123 124 // ââ Destroy ââââââââââââââââââââââââââââââââââââââââââââââââââ 125 return function destroy() { 126 container.removeEventListener('dragenter', onDragEnter); 127 container.removeEventListener('dragover', onDragOver); 128 container.removeEventListener('dragleave', onDragLeave); 129 container.removeEventListener('drop', onDropHandler); 130 131 overlay.remove(); 132 dragDepth = 0; 133 134 // Restore position only if we changed it 135 if (!prevPosition || prevPosition === 'static') { 136 container.style.position = prevPosition || ''; 137 } 138 }; 139}
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.