useFileInput.tsx 4.2 KB

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