useFileInput.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { useCallback, useRef, useState } from "react";
  2. /**
  3. * TODO (MR): Understand how this is happening, and validate it further (on
  4. * first glance this is correct).
  5. *
  6. * [Note: File paths when running under Electron]
  7. *
  8. * We have access to the absolute path of the web {@link File} object when we
  9. * are running in the context of our desktop app.
  10. *
  11. * This is in contrast to the `webkitRelativePath` that we get when we're
  12. * running in the browser, which is the relative path to the directory that the
  13. * user selected (or just the name of the file if the user selected or
  14. * drag/dropped a single one).
  15. */
  16. export interface FileWithPath extends File {
  17. readonly path?: string;
  18. }
  19. interface UseFileInputParams {
  20. directory?: boolean;
  21. accept?: string;
  22. }
  23. /**
  24. * Return three things:
  25. *
  26. * - A function that can be called to trigger the showing of the select file /
  27. * directory dialog.
  28. *
  29. * - The list of properties that should be passed to a dummy `input` element
  30. * that needs to be created to anchor the select file dialog. This input HTML
  31. * element is not going to be visible, but it needs to be part of the DOM fro
  32. * the open trigger to have effect.
  33. *
  34. * - The list of files that the user selected. This will be a list even if the
  35. * user selected directories - in that case, it will be the recursive list of
  36. * files within this directory.
  37. *
  38. * @param param0
  39. *
  40. * - If {@link directory} is true, the file open dialog will ask the user to
  41. * select directories. Otherwise it'll ask the user to select files.
  42. *
  43. * - If {@link accept} is specified, it'll restrict the type of files that the
  44. * user can select by setting the "accept" attribute of the underlying HTML
  45. * input element we use to surface the file selector dialog. For value of
  46. * accept can be an extension or a MIME type (See
  47. * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
  48. */
  49. export default function useFileInput({ directory, accept }: UseFileInputParams) {
  50. const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
  51. const inputRef = useRef<HTMLInputElement>();
  52. const openSelectorDialog = useCallback(() => {
  53. if (inputRef.current) {
  54. inputRef.current.value = null;
  55. inputRef.current.click();
  56. }
  57. }, []);
  58. const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
  59. event,
  60. ) => {
  61. if (!!event.target && !!event.target.files) {
  62. const files = [...event.target.files].map((file) =>
  63. toFileWithPath(file),
  64. );
  65. setSelectedFiles(files);
  66. }
  67. };
  68. const getInputProps = useCallback(
  69. () => ({
  70. type: "file",
  71. multiple: true,
  72. style: { display: "none" },
  73. ...(directory ? { directory: "", webkitdirectory: "" } : {}),
  74. ref: inputRef,
  75. onChange: handleChange,
  76. ...(accept ? { accept } : {}),
  77. }),
  78. [],
  79. );
  80. return {
  81. getInputProps,
  82. open: openSelectorDialog,
  83. selectedFiles: selectedFiles,
  84. };
  85. }
  86. // https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L88
  87. export function toFileWithPath(file: File, path?: string): FileWithPath {
  88. if (typeof (file as any).path !== "string") {
  89. // on electron, path is already set to the absolute path
  90. const { webkitRelativePath } = file;
  91. Object.defineProperty(file, "path", {
  92. value:
  93. typeof path === "string"
  94. ? path
  95. : typeof webkitRelativePath === "string" && // If <input webkitdirectory> is set,
  96. // the File will have a {webkitRelativePath} property
  97. // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
  98. webkitRelativePath.length > 0
  99. ? webkitRelativePath
  100. : file.name,
  101. writable: false,
  102. configurable: false,
  103. enumerable: true,
  104. });
  105. }
  106. return file;
  107. }