PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/clipboard-and-drop.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:18 UTC 4,940 bytes, 133 lines download raw bytes

1/**
2 * Paste + drag-and-drop import handlers. Both add an image to the
3 * editor as a new layer:
4 *
5 *   - Paste (Ctrl+V): checks `state.internalClipboard` first (set by
6 *     lasso copy/cut), then falls back to the system clipboard's
7 *     `image/*` items. Layer is named "Pasted Selection" or "Pasted"
8 *     and becomes active; the tool snaps to Move so the user can
9 *     reposition it immediately.
10 *   - Drop: any `image/*` file dragged from the OS / another tab.
11 *     Shows a "Drop image to add as new layer" overlay mid-drag. Each
12 *     dropped image is routed through `handleImportedImage` so canvas-
13 *     resize prompts + undo history work the same as the toolbar
14 *     Import button.
15 *
16 * Both gated by `state.editorOpen` so they're inert when the editor
17 * is closed (other listeners on the page get first dibs).
18 *
19 * @param {{
20 *   container:            HTMLElement,
21 *   saveState:            (label?: string) => void,
22 *   createLayer:          (name: string, w: number, h: number) => object,
23 *   renderLayerPanel:     () => void,
24 *   composite:            () => void,
25 *   handleImportedImage:  (img: HTMLImageElement) => void,
26 *   uiModule:             object,
27 * }} deps
28 */
29import { state } from './state.js';
30
31export function wireClipboardAndDrop({
32  container, saveState, createLayer, renderLayerPanel, composite,
33  handleImportedImage, uiModule,
34}) {
35  // ── Paste ──
36  window.addEventListener('paste', (e) => {
37    if (!state.editorOpen) return;
38
39    function pasteAsLayer(imgSource, label) {
40      if (!state.editorOpen) return; // user closed mid-paste
41      saveState();
42      const layer = createLayer(label || 'Pasted', imgSource.width, imgSource.height);
43      layer.ctx.drawImage(imgSource, 0, 0);
44      state.layers.push(layer);
45      state.activeLayerId = layer.id;
46      state.tool = 'move';
47      const tb = state.container?.querySelector('.ge-toolbar');
48      if (tb) tb.querySelectorAll('.ge-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === 'move'));
49      renderLayerPanel();
50      composite();
51      uiModule.showToast('Pasted as new layer');
52    }
53
54    // Check internal clipboard first (from Ctrl+C lasso/wand).
55    if (state.internalClipboard) {
56      e.preventDefault();
57      e.stopImmediatePropagation();
58      pasteAsLayer(state.internalClipboard, 'Pasted Selection');
59      return;
60    }
61
62    // Fall back to system clipboard.
63    const items = e.clipboardData?.items;
64    if (!items) return;
65    for (const item of items) {
66      if (!item.type.startsWith('image/')) continue;
67      e.preventDefault();
68      e.stopImmediatePropagation();
69      const blob = item.getAsFile();
70      const url = URL.createObjectURL(blob);
71      const img = new Image();
72      img.onload = () => { pasteAsLayer(img, 'Pasted'); URL.revokeObjectURL(url); };
73      img.src = url;
74      break;
75    }
76  }, true);  // capture phase so we beat chat input
77
78  // ── Drag-and-drop ──
79  // Visual drop-zone overlay appears mid-drag; routes via
80  // handleImportedImage so the import respects canvas resizing rules
81  // + saves history (same path as the toolbar Import button).
82  const dropZone = container;
83  if (!dropZone) return;
84  let dragDepth = 0;
85  const hasFileType = (dt) => dt && Array.from(dt.types || []).some(t => t === 'Files');
86  const showOverlay = () => {
87    if (!state.editorOpen) return;
88    let ov = dropZone.querySelector('.ge-drop-overlay');
89    if (!ov) {
90      ov = document.createElement('div');
91      ov.className = 'ge-drop-overlay';
92      ov.innerHTML = '<div class="ge-drop-overlay-msg">Drop image to add as new layer</div>';
93      dropZone.appendChild(ov);
94    }
95    ov.style.display = '';
96  };
97  const hideOverlay = () => {
98    const ov = dropZone.querySelector('.ge-drop-overlay');
99    if (ov) ov.style.display = 'none';
100  };
101  dropZone.addEventListener('dragenter', (e) => {
102    if (!state.editorOpen || !hasFileType(e.dataTransfer)) return;
103    e.preventDefault();
104    dragDepth++;
105    showOverlay();
106  });
107  dropZone.addEventListener('dragover', (e) => {
108    if (!state.editorOpen || !hasFileType(e.dataTransfer)) return;
109    e.preventDefault();
110    e.dataTransfer.dropEffect = 'copy';
111  });
112  dropZone.addEventListener('dragleave', () => {
113    if (!state.editorOpen) return;
114    dragDepth = Math.max(0, dragDepth - 1);
115    if (dragDepth === 0) hideOverlay();
116  });
117  dropZone.addEventListener('drop', (e) => {
118    if (!state.editorOpen) return;
119    dragDepth = 0;
120    hideOverlay();
121    const files = Array.from(e.dataTransfer?.files || []).filter(f => f.type.startsWith('image/'));
122    if (!files.length) return;
123    e.preventDefault();
124    e.stopPropagation();
125    for (const f of files) {
126      const url = URL.createObjectURL(f);
127      const img = new Image();
128      img.onload = () => { handleImportedImage(img); URL.revokeObjectURL(url); };
129      img.onerror = () => URL.revokeObjectURL(url);
130      img.src = url;
131    }
132  });
133}

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.