You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
4.3 KiB
145 lines
4.3 KiB
import { MIME_TYPES } from "@excalidraw/excalidraw"; |
|
import { fileOpen as _fileOpen } from "browser-fs-access"; |
|
import { unstable_batchedUpdates } from "react-dom"; |
|
|
|
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); |
|
};
|
|
|