PageSourceSearch

https://smrtbld.ai/js/utils/passive-drop.js

js smrtbld.ai collected 2026-10-05 15:08:59 UTC 4,870 bytes, 139 lines download raw bytes

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.