aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/data/filesystem.ts
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/data/filesystem.ts
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/data/filesystem.ts')
-rw-r--r--packages/excalidraw/data/filesystem.ts104
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 };