You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
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<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 AbortError());
|
|
}
|
|
};
|
|
},
|
|
}) 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);
|
|
};
|