File size: 3,000 Bytes
3f22414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
 * <image-drop-zone> — 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, `
      <style>
        .drop-zone .drop-zone-area {
          border: 2px dashed var(--pico-muted-border-color, #444);
          border-radius: 8px; padding: 3rem; text-align: center;
          color: var(--pico-muted-color, #666); font-size: 0.9rem;
          cursor: pointer; transition: border-color 0.2s;
        }
        .drop-zone .drop-zone-area.dragover {
          border-color: var(--pico-primary, #4c8);
          color: var(--pico-primary, #4c8);
        }
      </style>
      <input type="file" accept="image/*" hidden>
      <div class="drop-zone-area">
        <i class="fas fa-cloud-upload-alt" style="font-size:1.5rem; display:block; margin-bottom:0.5rem"></i>
        Drop an image here, paste from clipboard, or click to browse
      </div>
    `);
  }
}

customElements.define('image-drop-zone', ImageDropZone);