diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/data/filesystem.ts | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/data/filesystem.ts')
| -rw-r--r-- | packages/excalidraw/data/filesystem.ts | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/packages/excalidraw/data/filesystem.ts b/packages/excalidraw/data/filesystem.ts new file mode 100644 index 0000000..186d587 --- /dev/null +++ b/packages/excalidraw/data/filesystem.ts @@ -0,0 +1,104 @@ +import type { FileSystemHandle } from "browser-fs-access"; +import { + fileOpen as _fileOpen, + fileSave as _fileSave, + supported as nativeFileSystemSupported, +} from "browser-fs-access"; +import { EVENT, MIME_TYPES } from "../constants"; +import { AbortError } from "../errors"; +import { debounce } from "../utils"; + +type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; + +const INPUT_CHANGE_INTERVAL_MS = 500; + +export const fileOpen = <M extends boolean | undefined = false>(opts: { + extensions?: FILE_EXTENSION[]; + description: string; + multiple?: M; +}): Promise<M extends false | undefined ? File : File[]> => { + // an unsafe TS hack, alas not much we can do AFAIK + type RetType = M extends false | undefined ? File : File[]; + + const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { + mimeTypes.push(MIME_TYPES[type]); + + return mimeTypes; + }, [] as string[]); + + const extensions = opts.extensions?.reduce((acc, ext) => { + if (ext === "jpg") { + return acc.concat(".jpg", ".jpeg"); + } + return acc.concat(`.${ext}`); + }, [] as string[]); + + return _fileOpen({ + description: opts.description, + extensions, + mimeTypes, + multiple: opts.multiple ?? false, + legacySetup: (resolve, reject, input) => { + const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); + const focusHandler = () => { + checkForFile(); + document.addEventListener(EVENT.KEYUP, scheduleRejection); + document.addEventListener(EVENT.POINTER_UP, scheduleRejection); + scheduleRejection(); + }; + const checkForFile = () => { + // this hack might not work when expecting multiple files + if (input.files?.length) { + const ret = opts.multiple ? [...input.files] : input.files[0]; + resolve(ret as RetType); + } + }; + requestAnimationFrame(() => { + window.addEventListener(EVENT.FOCUS, focusHandler); + }); + const interval = window.setInterval(() => { + checkForFile(); + }, INPUT_CHANGE_INTERVAL_MS); + return (rejectPromise) => { + clearInterval(interval); + scheduleRejection.cancel(); + window.removeEventListener(EVENT.FOCUS, focusHandler); + document.removeEventListener(EVENT.KEYUP, scheduleRejection); + document.removeEventListener(EVENT.POINTER_UP, scheduleRejection); + if (rejectPromise) { + // so that something is shown in console if we need to debug this + console.warn("Opening the file was canceled (legacy-fs)."); + rejectPromise(new AbortError()); + } + }; + }, + }) as Promise<RetType>; +}; + +export const fileSave = ( + blob: Blob | Promise<Blob>, + opts: { + /** supply without the extension */ + name: string; + /** file extension */ + extension: FILE_EXTENSION; + mimeTypes?: string[]; + description: string; + /** existing FileSystemHandle */ + fileHandle?: FileSystemHandle | null; + }, +) => { + return _fileSave( + blob, + { + fileName: `${opts.name}.${opts.extension}`, + description: opts.description, + extensions: [`.${opts.extension}`], + mimeTypes: opts.mimeTypes, + }, + opts.fileHandle, + ); +}; + +export { nativeFileSystemSupported }; +export type { FileSystemHandle }; |
