import { unstable_batchedUpdates } from "react-dom"; import { fileOpen as _fileOpen } from "browser-fs-access"; import { MIME_TYPES } from "@excalidraw/excalidraw"; import { AbortError } from "../../packages/excalidraw/errors"; type FILE_EXTENSION = Exclude; const INPUT_CHANGE_INTERVAL_MS = 500; export type ResolvablePromise = Promise & { resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; reject: (error: Error) => void; }; export const resolvablePromise = () => { let resolve!: any; let reject!: any; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); (promise as any).resolve = resolve; (promise as any).reject = reject; return promise as ResolvablePromise; }; export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { const xd = x2 - x1; const yd = y2 - y1; return Math.hypot(xd, yd); }; export const fileOpen = (opts: { extensions?: FILE_EXTENSION[]; description: string; multiple?: M; }): Promise => { // 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("keyup", scheduleRejection); document.addEventListener("pointerup", 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("focus", focusHandler); }); const interval = window.setInterval(() => { checkForFile(); }, INPUT_CHANGE_INTERVAL_MS); return (rejectPromise) => { clearInterval(interval); scheduleRejection.cancel(); window.removeEventListener("focus", focusHandler); document.removeEventListener("keyup", scheduleRejection); document.removeEventListener("pointerup", 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; }; export const debounce = ( fn: (...args: T) => void, timeout: number, ) => { let handle = 0; let lastArgs: T | null = null; const ret = (...args: T) => { lastArgs = args; clearTimeout(handle); handle = window.setTimeout(() => { lastArgs = null; fn(...args); }, timeout); }; ret.flush = () => { clearTimeout(handle); if (lastArgs) { const _lastArgs = lastArgs; lastArgs = null; fn(..._lastArgs); } }; ret.cancel = () => { lastArgs = null; clearTimeout(handle); }; return ret; }; export const withBatchedUpdates = < TFunction extends ((event: any) => void) | (() => void), >( func: Parameters["length"] extends 0 | 1 ? TFunction : never, ) => ((event) => { unstable_batchedUpdates(func as TFunction, event); }) as TFunction; /** * barches React state updates and throttles the calls to a single call per * animation frame */ export const withBatchedUpdatesThrottled = < TFunction extends ((event: any) => void) | (() => void), >( func: Parameters["length"] extends 0 | 1 ? TFunction : never, ) => { // @ts-ignore return throttleRAF>(((event) => { unstable_batchedUpdates(func, event); }) as TFunction); };