| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 | import React, { CSSProperties, FC, HTMLAttributes, InputHTMLAttributes, useEffect, useRef, useState } from 'react';const supportsInputDirs = 'webkitdirectory' in HTMLInputElement.prototype;const supportsRelativePath = 'webkitRelativePath' in File.prototype;const supportsDirs = typeof DataTransferItem != 'undefined' && 'webkitGetAsEntry' in DataTransferItem.prototype;const readRecurse = (dir: FileSystemDirectoryEntry, onComplete: (files: File[]) => void, onError: (err: Error) => void) => {  let files: File[] = [];  let total = 0;  let errored = false;  let reachedEnd = false;  const onErr = (err: Error) => {    if (!errored) {      errored = true;      onError(err);    }  };  const onDone = (f: File[]) => {    files = files.concat(f);    if (!--total && reachedEnd) onComplete(files);  };  const reader = dir.createReader();  const onRead = (entries: FileSystemEntry[]) => {    if (!entries.length && !errored) {      if (!total) onComplete(files);      else reachedEnd = true;    } else reader.readEntries(onRead, onError);    for (const entry of entries) {      ++total;      if (entry.isFile) entry.file(f => onDone([        new File([f], entry.fullPath.slice(1), f)      ]), onErr);      else readRecurse(entry as FileSystemDirectoryEntry, onDone, onErr);    }  };  reader.readEntries(onRead, onError);}const FilePicker: FC<{  onFiles(files: File[] | null): void;  onDrag(on: boolean): void;  onError(err: string | Error): void;  allowDirs: boolean;} & Omit<HTMLAttributes<HTMLDivElement>, 'onError'>> = ({ onFiles, onDrag, onError, style, allowDirs, children, ...props }) => {  const inputRef = useRef<HTMLInputElement>(null);  const dirInputRef = useRef<HTMLInputElement>(null);  const dragRef = useRef(0);  const [inputHover, setInputHover] = useState(false);  const [dirInputHover, setDirInputHover] = useState(false);  const [isHovering, setIsHovering] = useState(false);  useEffect(() => {    // only init'd when support dirs    if (dirInputRef.current) {      dirInputRef.current.setAttribute('webkitdirectory', '');    }  }, []);  const rootProps: HTMLAttributes<HTMLDivElement> = {    onDrop(ev) {      ev.preventDefault();      const tf = ev.dataTransfer;      if (!tf.files.length) onError('Please drop some files in');      else {        onFiles(null);        if (supportsDirs && allowDirs) {          let outFiles: File[] = [];          let lft = tf.items.length;          let errored = false;          const onErr = (err: Error) => {            if (!errored) {              errored = true;              onError(err);            }          }          const onDone = (f: File[]) => {            outFiles = outFiles.concat(f);            if (!--lft && !errored) onFiles(outFiles);          };          for (let i = 0; i < tf.items.length; ++i) {            const entry = tf.items[i].webkitGetAsEntry();            if (entry.isFile) entry.file(f => onDone([f]), onErr);            else readRecurse(entry as FileSystemDirectoryEntry, onDone, onErr);          }        } else onFiles(Array.prototype.slice.call(tf.files));      }      setIsHovering(false);    },    onDragEnter() {      ++dragRef.current;      onDrag(true);      setIsHovering(true);    },    onDragOver(ev) {      ev.preventDefault();    },    onDragLeave() {      if (!--dragRef.current) {        onDrag(false);          setIsHovering(false);      }    },    style: {      display: 'flex',      flexDirection: 'column',      alignItems: 'center',      ...style    }  };  const inputProps: InputHTMLAttributes<HTMLInputElement> = {    onInput(ev) {      const t = ev.currentTarget, files = t.files!;      if (supportsRelativePath) {        const outFiles: File[] = Array(files.length);        for (let i = 0; i < files.length; ++i) {          const file = files[i];          outFiles[i] = new File([file], file.webkitRelativePath || file.name, file);        }        onFiles(outFiles);      } else onFiles(Array.prototype.slice.call(files));      t.value = '';    },    style: { display: 'none' },    multiple: true  };  const buttonStyles: CSSProperties = {    cursor: 'default',    minWidth: '8vw',    height: '6vh',    margin: '1vmin',    padding: '1vmin',    display: 'flex',    alignItems: 'center',    justifyContent: 'center',    boxShadow: '0 1px 2px 1px rgba(0, 0, 0, 0.2), 0 2px 4px 2px rgba(0, 0, 0, 0.15), 0 4px 8px 4px rgba(0, 0, 0, 0.12)',    border: '1px solid black',    borderRadius: '6px',    transition: 'background-color 300ms ease-in-out',    WebkitTouchCallout: 'none',    WebkitUserSelect: 'none',    msUserSelect: 'none',    MozUserSelect: 'none',    userSelect: 'none'  };  return (    <div {...props} {...rootProps}>      {children}      <div style={{        transition: 'transform ' + (isHovering ? 300 : 50) + 'ms ease-in-out',        transform: isHovering ? 'scale(1.5)' : 'none'      }}>Drag and Drop</div>      <div style={{        borderBottom: '1px solid gray',        margin: '1.5vh',        color: 'gray',        lineHeight: 0,        paddingTop: '1.5vh',        marginBottom: '3vh',        width: '100%',      }}>        <span style={{ background: 'white', padding: '0.25em' }}>OR</span>      </div>      <div style={{        display: 'flex',        flexDirection: 'row',        alignItems: 'center',        justifyContent: 'center',        width: '100%'      }}>        <input type="file" ref={inputRef} {...inputProps} />        <div onClick={() => inputRef.current!.click()} onMouseOver={() => setInputHover(true)} onMouseOut={() => setInputHover(false)} style={{          ...buttonStyles,          backgroundColor: inputHover ? 'rgba(0, 0, 0, 0.14)' : 'white'        }}>Select Files</div>        {supportsInputDirs && allowDirs &&          <>            <div style={{ boxShadow: '1px 0 black', height: '100%' }}><span /></div>            <input type="file" ref={dirInputRef} {...inputProps} />            <div onClick={() => dirInputRef.current!.click()} onMouseOver={() => setDirInputHover(true)} onMouseOut={() => setDirInputHover(false)} style={{              ...buttonStyles,              marginLeft: '8vmin',              backgroundColor: dirInputHover ? 'rgba(0, 0, 0, 0.14)' : 'white'            }}>Select Folders</div>          </>        }      </div>    </div>  );}export default FilePicker;
 |