useFileInput.tsx 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  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. export default function useFileInput({ directory }: { directory?: boolean }) {
  20. const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
  21. const inputRef = useRef<HTMLInputElement>();
  22. const openSelectorDialog = useCallback(() => {
  23. if (inputRef.current) {
  24. inputRef.current.value = null;
  25. inputRef.current.click();
  26. }
  27. }, []);
  28. const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
  29. event,
  30. ) => {
  31. if (!!event.target && !!event.target.files) {
  32. const files = [...event.target.files].map((file) =>
  33. toFileWithPath(file),
  34. );
  35. setSelectedFiles(files);
  36. }
  37. };
  38. const getInputProps = useCallback(
  39. () => ({
  40. type: "file",
  41. multiple: true,
  42. style: { display: "none" },
  43. ...(directory ? { directory: "", webkitdirectory: "" } : {}),
  44. ref: inputRef,
  45. onChange: handleChange,
  46. }),
  47. [],
  48. );
  49. return {
  50. getInputProps,
  51. open: openSelectorDialog,
  52. selectedFiles: selectedFiles,
  53. };
  54. }
  55. // https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L88
  56. export function toFileWithPath(file: File, path?: string): FileWithPath {
  57. if (typeof (file as any).path !== "string") {
  58. // on electron, path is already set to the absolute path
  59. const { webkitRelativePath } = file;
  60. Object.defineProperty(file, "path", {
  61. value:
  62. typeof path === "string"
  63. ? path
  64. : typeof webkitRelativePath === "string" && // If <input webkitdirectory> is set,
  65. // the File will have a {webkitRelativePath} property
  66. // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
  67. webkitRelativePath.length > 0
  68. ? webkitRelativePath
  69. : file.name,
  70. writable: false,
  71. configurable: false,
  72. enumerable: true,
  73. });
  74. }
  75. return file;
  76. }