useFileInput.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  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. setSelectedFiles([...event.target.files]);
  71. }
  72. };
  73. // [Note: webkitRelativePath]
  74. //
  75. // If the webkitdirectory attribute of an <input> HTML element is set then
  76. // the File objects that we get will have `webkitRelativePath` property
  77. // containing the relative path to the selected directory.
  78. //
  79. // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
  80. const directoryOpts = directory
  81. ? { directory: "", webkitdirectory: "" }
  82. : {};
  83. const getInputProps = useCallback(
  84. () => ({
  85. type: "file",
  86. multiple: true,
  87. style: { display: "none" },
  88. ...directoryOpts,
  89. ref: inputRef,
  90. onChange: handleChange,
  91. ...(accept ? { accept } : {}),
  92. }),
  93. [],
  94. );
  95. return {
  96. getInputProps,
  97. open: openSelectorDialog,
  98. selectedFiles: selectedFiles,
  99. };
  100. }