summaryrefslogtreecommitdiff
path: root/fflate/demo/components/file-picker/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'fflate/demo/components/file-picker/index.tsx')
-rw-r--r--fflate/demo/components/file-picker/index.tsx190
1 files changed, 190 insertions, 0 deletions
diff --git a/fflate/demo/components/file-picker/index.tsx b/fflate/demo/components/file-picker/index.tsx
new file mode 100644
index 0000000..b88aad4
--- /dev/null
+++ b/fflate/demo/components/file-picker/index.tsx
@@ -0,0 +1,190 @@
+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; \ No newline at end of file