import { type FontImportDeps, importFonts, isFontFileName } from './fontImport'; import { importImages, type TextureImportDeps } from './textureImport'; export type FileImportDeps = TextureImportDeps & FontImportDeps; /** * Wires drop-onto-`target` and paste, and returns the remover for both. Paste is bound to the * window because the canvas cannot hold focus, and skipped while a DOM text field has it. * * Pictures and typefaces arrive the same way, so one pair of handlers sorts them apart. */ export function installFileImport(target: HTMLElement, deps: FileImportDeps): () => void { const onDragOver = (event: DragEvent) => { if (!hasImportableItems(event.dataTransfer)) return; // Without this the browser navigates away to the dropped file. event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; }; const onDrop = (event: DragEvent) => { const images = imageFilesOf(event.dataTransfer); const fonts = fontFilesOf(event.dataTransfer); if (images.length === 0 && fonts.length === 0) { if (!event.dataTransfer?.files.length) return; event.preventDefault(); deps.state.setNotice('Only image and font files can be dropped into the editor.'); return; } event.preventDefault(); run(images, fonts, deps); }; const onPaste = (event: ClipboardEvent) => { if (isTextFieldFocused()) return; const images = imageFilesOf(event.clipboardData); const fonts = fontFilesOf(event.clipboardData); if (images.length === 0 && fonts.length === 0) return; event.preventDefault(); run(images, fonts, deps); }; target.addEventListener('dragover', onDragOver); target.addEventListener('drop', onDrop); window.addEventListener('paste', onPaste); return () => { target.removeEventListener('dragover', onDragOver); target.removeEventListener('drop', onDrop); window.removeEventListener('paste', onPaste); }; } /** Images first, then fonts, so a mixed drop ends on one notice instead of two racing ones. */ function run(images: readonly File[], fonts: readonly File[], deps: FileImportDeps): void { const work = async () => { if (images.length) await importImages(images, deps); if (fonts.length) await importFonts(fonts, deps); }; void work().catch((error: unknown) => deps.state.setNotice(String(error), 'error')); } function imageFilesOf(data: DataTransfer | null): File[] { return [...(data?.files ?? [])].filter((file) => file.type.startsWith('image/')); } function fontFilesOf(data: DataTransfer | null): File[] { return [...(data?.files ?? [])].filter((file) => isFontFileName(file.name)); } /** * During a drag the files themselves are not readable yet – only kind and type are, and a font * usually has no type at all. Anything that is not plainly text therefore counts as a candidate; * the drop decides, and by then the names are readable. */ function hasImportableItems(data: DataTransfer | null): boolean { return [...(data?.items ?? [])].some((item) => item.kind === 'file' && !item.type.startsWith('text/')); } function isTextFieldFocused(): boolean { const active = document.activeElement; return active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement; }