filesystem.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import {
  2. fileOpen as _fileOpen,
  3. fileSave as _fileSave,
  4. FileSystemHandle,
  5. supported as nativeFileSystemSupported,
  6. } from "browser-fs-access";
  7. import { EVENT, MIME_TYPES } from "../constants";
  8. import { AbortError } from "../errors";
  9. import { debounce } from "../utils";
  10. type FILE_EXTENSION =
  11. | "gif"
  12. | "jpg"
  13. | "png"
  14. | "excalidraw.png"
  15. | "svg"
  16. | "excalidraw.svg"
  17. | "json"
  18. | "excalidraw"
  19. | "excalidrawlib";
  20. const INPUT_CHANGE_INTERVAL_MS = 500;
  21. export const fileOpen = <M extends boolean | undefined = false>(opts: {
  22. extensions?: FILE_EXTENSION[];
  23. description: string;
  24. multiple?: M;
  25. }): Promise<M extends false | undefined ? File : File[]> => {
  26. // an unsafe TS hack, alas not much we can do AFAIK
  27. type RetType = M extends false | undefined ? File : File[];
  28. const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
  29. mimeTypes.push(MIME_TYPES[type]);
  30. return mimeTypes;
  31. }, [] as string[]);
  32. const extensions = opts.extensions?.reduce((acc, ext) => {
  33. if (ext === "jpg") {
  34. return acc.concat(".jpg", ".jpeg");
  35. }
  36. return acc.concat(`.${ext}`);
  37. }, [] as string[]);
  38. return _fileOpen({
  39. description: opts.description,
  40. extensions,
  41. mimeTypes,
  42. multiple: opts.multiple ?? false,
  43. legacySetup: (resolve, reject, input) => {
  44. const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
  45. const focusHandler = () => {
  46. checkForFile();
  47. document.addEventListener(EVENT.KEYUP, scheduleRejection);
  48. document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
  49. scheduleRejection();
  50. };
  51. const checkForFile = () => {
  52. // this hack might not work when expecting multiple files
  53. if (input.files?.length) {
  54. const ret = opts.multiple ? [...input.files] : input.files[0];
  55. resolve(ret as RetType);
  56. }
  57. };
  58. requestAnimationFrame(() => {
  59. window.addEventListener(EVENT.FOCUS, focusHandler);
  60. });
  61. const interval = window.setInterval(() => {
  62. checkForFile();
  63. }, INPUT_CHANGE_INTERVAL_MS);
  64. return (rejectPromise) => {
  65. clearInterval(interval);
  66. scheduleRejection.cancel();
  67. window.removeEventListener(EVENT.FOCUS, focusHandler);
  68. document.removeEventListener(EVENT.KEYUP, scheduleRejection);
  69. document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
  70. if (rejectPromise) {
  71. // so that something is shown in console if we need to debug this
  72. console.warn("Opening the file was canceled (legacy-fs).");
  73. rejectPromise(new AbortError());
  74. }
  75. };
  76. },
  77. }) as Promise<RetType>;
  78. };
  79. export const fileSave = (
  80. blob: Blob,
  81. opts: {
  82. /** supply without the extension */
  83. name: string;
  84. /** file extension */
  85. extension: FILE_EXTENSION;
  86. description: string;
  87. /** existing FileSystemHandle */
  88. fileHandle?: FileSystemHandle | null;
  89. },
  90. ) => {
  91. return _fileSave(
  92. blob,
  93. {
  94. fileName: `${opts.name}.${opts.extension}`,
  95. description: opts.description,
  96. extensions: [`.${opts.extension}`],
  97. },
  98. opts.fileHandle,
  99. );
  100. };
  101. export type { FileSystemHandle };
  102. export { nativeFileSystemSupported };