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.