/* ===== Clipboard Paste Support ===== */ window.NAIPaste = { /** * Registry of paste targets. * Each entry: { sel, page, handler } */ targets: [], /** The most recently focused/interacted dropzone target (within current page) */ _lastActiveTarget: null, /** * Register a single-image dropzone as a paste target. */ register(dropzoneSelector, pageId, handler) { const entry = { sel: dropzoneSelector, page: pageId, handler }; this.targets.push(entry); const dz = document.querySelector(dropzoneSelector); if (!dz) return; // Make focusable dz.setAttribute('tabindex', '0'); // Track interaction: click, focus, dragover all mark this as "last active" const markActive = () => { this._lastActiveTarget = entry; }; dz.addEventListener('focus', markActive); dz.addEventListener('click', markActive); dz.addEventListener('dragover', markActive); dz.addEventListener('mouseenter', markActive); // Direct paste when dropzone has focus dz.addEventListener('paste', (e) => { const file = this._extractImageFile(e); if (file) { e.preventDefault(); e.stopPropagation(); handler(file); } }); }, _extractImageFile(e) { const items = e.clipboardData?.items; if (!items) return null; for (const item of items) { if (item.type.startsWith('image/')) return item.getAsFile(); } return null; }, _currentPage() { return NAIState.currentPage || 'generate'; }, /** * Find paste target priority: * 1. Last actively interacted dropzone on current page (if its panel is open) * 2. Last open-panel dropzone on current page (bottom-up) * 3. Fallback: first target on current page */ _findTarget() { const currentPage = this._currentPage(); const candidates = this.targets.filter(t => t.page === currentPage); if (candidates.length === 0) return null; // 1. Last actively interacted (if on current page and panel is open) if (this._lastActiveTarget && this._lastActiveTarget.page === currentPage) { const dz = document.querySelector(this._lastActiveTarget.sel); if (dz && this._isDropzoneAccessible(dz)) { return this._lastActiveTarget; } } // 2. Last open-panel dropzone (bottom-up order) for (let i = candidates.length - 1; i >= 0; i--) { const t = candidates[i]; const dz = document.querySelector(t.sel); if (dz && this._isDropzoneAccessible(dz)) return t; } // 3. Fallback return candidates[0]; }, /** * Check if a dropzone is accessible (visible, and its collapsible parent is open). */ _isDropzoneAccessible(dz) { const collapsible = dz.closest('.collapsible-content'); // Not inside a collapsible → always accessible if (!collapsible) return true; return collapsible.classList.contains('open'); }, init() { // Register generate page targets this.register('#extra-input-dropzone', 'generate', (file) => NAIExtraInput.handleUpload(file)); this.register('#reference-dropzone', 'generate', (file) => NAIReference.handleUpload(file)); // Reset last active target on page switch const origSwitch = NAIState._onPageSwitch; document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', () => { this._lastActiveTarget = null; }); }); // Global paste listener document.addEventListener('paste', (e) => { const active = document.activeElement; if (active) { const tag = active.tagName; if (tag === 'TEXTAREA') return; if (tag === 'INPUT' && ['text', 'number', 'search'].includes(active.type)) return; } const file = this._extractImageFile(e); if (!file) return; const target = this._findTarget(); if (target) { e.preventDefault(); target.handler(file); } }); }, };