utils.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { MIME_TYPES } from "@excalidraw/excalidraw";
  2. import { fileOpen as _fileOpen } from "browser-fs-access";
  3. import { unstable_batchedUpdates } from "react-dom";
  4. type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
  5. const INPUT_CHANGE_INTERVAL_MS = 500;
  6. export type ResolvablePromise<T> = Promise<T> & {
  7. resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
  8. reject: (error: Error) => void;
  9. };
  10. export const resolvablePromise = <T>() => {
  11. let resolve!: any;
  12. let reject!: any;
  13. const promise = new Promise((_resolve, _reject) => {
  14. resolve = _resolve;
  15. reject = _reject;
  16. });
  17. (promise as any).resolve = resolve;
  18. (promise as any).reject = reject;
  19. return promise as ResolvablePromise<T>;
  20. };
  21. export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
  22. const xd = x2 - x1;
  23. const yd = y2 - y1;
  24. return Math.hypot(xd, yd);
  25. };
  26. export const fileOpen = <M extends boolean | undefined = false>(opts: {
  27. extensions?: FILE_EXTENSION[];
  28. description: string;
  29. multiple?: M;
  30. }): Promise<M extends false | undefined ? File : File[]> => {
  31. // an unsafe TS hack, alas not much we can do AFAIK
  32. type RetType = M extends false | undefined ? File : File[];
  33. const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
  34. mimeTypes.push(MIME_TYPES[type]);
  35. return mimeTypes;
  36. }, [] as string[]);
  37. const extensions = opts.extensions?.reduce((acc, ext) => {
  38. if (ext === "jpg") {
  39. return acc.concat(".jpg", ".jpeg");
  40. }
  41. return acc.concat(`.${ext}`);
  42. }, [] as string[]);
  43. return _fileOpen({
  44. description: opts.description,
  45. extensions,
  46. mimeTypes,
  47. multiple: opts.multiple ?? false,
  48. legacySetup: (resolve, reject, input) => {
  49. const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
  50. const focusHandler = () => {
  51. checkForFile();
  52. document.addEventListener("keyup", scheduleRejection);
  53. document.addEventListener("pointerup", scheduleRejection);
  54. scheduleRejection();
  55. };
  56. const checkForFile = () => {
  57. // this hack might not work when expecting multiple files
  58. if (input.files?.length) {
  59. const ret = opts.multiple ? [...input.files] : input.files[0];
  60. resolve(ret as RetType);
  61. }
  62. };
  63. requestAnimationFrame(() => {
  64. window.addEventListener("focus", focusHandler);
  65. });
  66. const interval = window.setInterval(() => {
  67. checkForFile();
  68. }, INPUT_CHANGE_INTERVAL_MS);
  69. return (rejectPromise) => {
  70. clearInterval(interval);
  71. scheduleRejection.cancel();
  72. window.removeEventListener("focus", focusHandler);
  73. document.removeEventListener("keyup", scheduleRejection);
  74. document.removeEventListener("pointerup", scheduleRejection);
  75. if (rejectPromise) {
  76. // so that something is shown in console if we need to debug this
  77. console.warn("Opening the file was canceled (legacy-fs).");
  78. rejectPromise(new Error("Request Aborted"));
  79. }
  80. };
  81. },
  82. }) as Promise<RetType>;
  83. };
  84. export const debounce = <T extends any[]>(
  85. fn: (...args: T) => void,
  86. timeout: number,
  87. ) => {
  88. let handle = 0;
  89. let lastArgs: T | null = null;
  90. const ret = (...args: T) => {
  91. lastArgs = args;
  92. clearTimeout(handle);
  93. handle = window.setTimeout(() => {
  94. lastArgs = null;
  95. fn(...args);
  96. }, timeout);
  97. };
  98. ret.flush = () => {
  99. clearTimeout(handle);
  100. if (lastArgs) {
  101. const _lastArgs = lastArgs;
  102. lastArgs = null;
  103. fn(..._lastArgs);
  104. }
  105. };
  106. ret.cancel = () => {
  107. lastArgs = null;
  108. clearTimeout(handle);
  109. };
  110. return ret;
  111. };
  112. export const withBatchedUpdates = <
  113. TFunction extends ((event: any) => void) | (() => void),
  114. >(
  115. func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
  116. ) =>
  117. ((event) => {
  118. unstable_batchedUpdates(func as TFunction, event);
  119. }) as TFunction;
  120. /**
  121. * barches React state updates and throttles the calls to a single call per
  122. * animation frame
  123. */
  124. export const withBatchedUpdatesThrottled = <
  125. TFunction extends ((event: any) => void) | (() => void),
  126. >(
  127. func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
  128. ) => {
  129. // @ts-ignore
  130. return throttleRAF<Parameters<TFunction>>(((event) => {
  131. unstable_batchedUpdates(func, event);
  132. }) as TFunction);
  133. };