client / static /js /paste.js
P01yH3dr0n's picture
launch
774fe36
Raw
History Blame Contribute Delete
4.36 kB
/* ===== 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);
}
});
},
};