/** * — file picker + drag-and-drop image loader. * * Events: * image-loaded — detail: { image: HTMLImageElement } */ import { morph } from 'lib/morph'; class ImageDropZone extends HTMLElement { connectedCallback() { this.classList.add('drop-zone'); this.#render(); this.addEventListener('click', e => { if (e.target.closest('.drop-zone-area')) this.querySelector('input[type="file"]').click(); }); this.addEventListener('dragover', e => { if (e.target.closest('.drop-zone-area')) { e.preventDefault(); e.target.closest('.drop-zone-area').classList.add('dragover'); } }); this.addEventListener('dragleave', e => { if (e.target.closest('.drop-zone-area')) e.target.closest('.drop-zone-area').classList.remove('dragover'); }); this.addEventListener('drop', e => { const area = e.target.closest('.drop-zone-area'); if (!area) return; e.preventDefault(); area.classList.remove('dragover'); if (e.dataTransfer.files.length) this.#handleFile(e.dataTransfer.files[0]); }); this.addEventListener('change', e => { if (e.target.matches('input[type="file"]') && e.target.files.length) this.#handleFile(e.target.files[0]); }); document.addEventListener('paste', e => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith('image/')) { e.preventDefault(); this.#handleFile(item.getAsFile()); return; } } }); } #handleFile(file) { if (!file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { this.dispatchEvent(new CustomEvent('image-loaded', { bubbles: true, detail: { image: img } })); }; img.src = reader.result; }; reader.readAsDataURL(file); } show() { this.style.display = ''; const input = this.querySelector('input[type="file"]'); if (input) input.value = ''; } hide() { this.style.display = 'none'; } #render() { morph(this, `
Drop an image here, paste from clipboard, or click to browse
`); } } customElements.define('image-drop-zone', ImageDropZone);