aboutsummaryrefslogtreecommitdiffstats
path: root/examples/with-script-in-browser/utils.ts
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit225db4a7805befe009fe055fc2ef5daedd6c04f9 (patch)
treea5b0d073daabaadceb2f5c1b18640d785b5a9c71 /examples/with-script-in-browser/utils.ts
parent8ff10d2bf233608b027f8503cb9c7100c9ee3f16 (diff)
refactor: examples/
Diffstat (limited to 'examples/with-script-in-browser/utils.ts')
-rw-r--r--examples/with-script-in-browser/utils.ts145
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);
+};