filesystem.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import {
  2. FileWithHandle,
  3. fileOpen as _fileOpen,
  4. fileSave as _fileSave,
  5. FileSystemHandle,
  6. supported as nativeFileSystemSupported,
  7. } from "browser-fs-access";
  8. import { EVENT, MIME_TYPES } from "../constants";
  9. import { AbortError } from "../errors";
  10. import { debounce } from "../utils";
  11. type FILE_EXTENSION =
  12. | "gif"
  13. | "jpg"
  14. | "png"
  15. | "excalidraw.png"
  16. | "svg"
  17. | "excalidraw.svg"
  18. | "json"
  19. | "excalidraw"
  20. | "excalidrawlib";
  21. const INPUT_CHANGE_INTERVAL_MS = 500;
  22. export const fileOpen = <M extends boolean | undefined = false>(opts: {
  23. extensions?: FILE_EXTENSION[];
  24. description: string;
  25. multiple?: M;
  26. }): Promise<
  27. M extends false | undefined ? FileWithHandle : FileWithHandle[]
  28. > => {
  29. // an unsafe TS hack, alas not much we can do AFAIK
  30. type RetType = M extends false | undefined
  31. ? FileWithHandle
  32. : FileWithHandle[];
  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(EVENT.KEYUP, scheduleRejection);
  53. document.addEventListener(EVENT.POINTER_UP, 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(EVENT.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(EVENT.FOCUS, focusHandler);
  73. document.removeEventListener(EVENT.KEYUP, scheduleRejection);
  74. document.removeEventListener(EVENT.POINTER_UP, 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 AbortError());
  79. }
  80. };
  81. },
  82. }) as Promise<RetType>;
  83. };
  84. export const fileSave = (
  85. blob: Blob,
  86. opts: {
  87. /** supply without the extension */
  88. name: string;
  89. /** file extension */
  90. extension: FILE_EXTENSION;
  91. description: string;
  92. /** existing FileSystemHandle */
  93. fileHandle?: FileSystemHandle | null;
  94. },
  95. ) => {
  96. return _fileSave(
  97. blob,
  98. {
  99. fileName: `${opts.name}.${opts.extension}`,
  100. description: opts.description,
  101. extensions: [`.${opts.extension}`],
  102. },
  103. opts.fileHandle,
  104. );
  105. };
  106. export type { FileSystemHandle };
  107. export { nativeFileSystemSupported };