diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 225db4a7805befe009fe055fc2ef5daedd6c04f9 (patch) | |
| tree | a5b0d073daabaadceb2f5c1b18640d785b5a9c71 /examples/with-script-in-browser/utils.ts | |
| parent | 8ff10d2bf233608b027f8503cb9c7100c9ee3f16 (diff) | |
refactor: examples/
Diffstat (limited to 'examples/with-script-in-browser/utils.ts')
| -rw-r--r-- | examples/with-script-in-browser/utils.ts | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/examples/with-script-in-browser/utils.ts b/examples/with-script-in-browser/utils.ts new file mode 100644 index 0000000..a77b93f --- /dev/null +++ b/examples/with-script-in-browser/utils.ts @@ -0,0 +1,145 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { fileOpen as _fileOpen } from "browser-fs-access"; +import { MIME_TYPES } from "@excalidraw/excalidraw"; + +type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; + +const INPUT_CHANGE_INTERVAL_MS = 500; + +export type ResolvablePromise<T> = Promise<T> & { + resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; + reject: (error: Error) => void; +}; +export const resolvablePromise = <T>() => { + 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<T>; +}; + +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 = <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("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 Error("Request Aborted")); + } + }; + }, + }) as Promise<RetType>; +}; + +export const debounce = <T extends any[]>( + 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<TFunction>["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<TFunction>["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF<Parameters<TFunction>>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; |
