react-native-surveys
Version:
Build your own forms, surveys and polls for your React Native apps.
484 lines (434 loc) • 12.5 kB
JavaScript
/* eslint-disable */
import React, {
forwardRef,
Fragment,
useCallback,
useEffect,
useMemo,
useReducer,
useRef
} from "react";
import { fromEvent } from "./file-selector";
import {
allFilesAccepted,
composeEventHandlers,
fileAccepted,
fileMatchSize,
isEvtWithFiles,
isIeOrEdge,
isPropagationStopped,
onDocumentDragOver
} from "./utils";
const Dropzone = forwardRef(({ children, ...params }, ref) => {
const { open, ...props } = useDropzone(params);
useEffect(() => {
if (typeof ref === "function") {
ref({
open
});
} else if (typeof ref === "object" && ref !== null) {
ref.current = {
open
};
}
return () => {
if (typeof ref === "function") {
ref(null);
} else if (typeof ref === "object" && ref !== null) {
ref.current = null;
}
};
}, [ref]); // TODO: Figure out why react-styleguidist cannot create docs if we don't return a jsx element
return React.createElement(Fragment, null, children({ ...props, open }));
});
Dropzone.displayName = "Dropzone";
export default Dropzone;
const initialState = {
isFocused: false,
isFileDialogActive: false,
isDragActive: false,
isDragAccept: false,
isDragReject: false,
draggedFiles: [],
acceptedFiles: [],
rejectedFiles: []
};
export function useDropzone({
accept,
disabled = false,
getFilesFromEvent = fromEvent,
maxSize = Infinity,
minSize = 0,
multiple = true,
onDragEnter,
onDragLeave,
onDragOver,
onDrop,
onDropAccepted,
onDropRejected,
onFileDialogCancel,
preventDropOnDocument = true,
noClick = false,
noKeyboard = false,
noDrag = false,
noDragEventsBubbling = false
} = {}) {
const rootRef = useRef(null);
const inputRef = useRef(null);
const [state, dispatch] = useReducer(reducer, initialState);
const { isFocused, isFileDialogActive, draggedFiles } = state; // Fn for opening the file dialog programmatically
const openFileDialog = () => {
if (inputRef.current) {
dispatch({
type: "openDialog"
});
inputRef.current.value = null;
inputRef.current.click();
}
}; // Update file dialog active state when the window is focused on
const onWindowFocus = () => {
// Execute the timeout only if the file dialog is opened in the browser
if (isFileDialogActive) {
setTimeout(() => {
if (inputRef.current) {
const { files } = inputRef.current;
if (!files.length) {
dispatch({
type: "closeDialog"
});
if (typeof onFileDialogCancel === "function") {
onFileDialogCancel();
}
}
}
}, 300);
}
};
useEffect(() => {
window.addEventListener("focus", onWindowFocus, false);
return () => {
window.removeEventListener("focus", onWindowFocus, false);
};
}, [inputRef, isFileDialogActive, onFileDialogCancel]); // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
const onKeyDownCb = useCallback(
event => {
// Ignore keyboard events bubbling up the DOM tree
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) {
return;
}
if (event.keyCode === 32 || event.keyCode === 13) {
event.preventDefault();
openFileDialog();
}
},
[rootRef, inputRef]
); // Update focus state for the dropzone
const onFocusCb = useCallback(() => {
dispatch({
type: "focus"
});
}, []);
const onBlurCb = useCallback(() => {
dispatch({
type: "blur"
});
}, []); // Cb to open the file dialog when click occurs on the dropzone
const onClickCb = useCallback(() => {
if (noClick) {
return;
} // In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()
// to ensure React can handle state changes
// See: https://github.com/react-dropzone/react-dropzone/issues/450
if (isIeOrEdge()) {
setTimeout(openFileDialog, 0);
} else {
openFileDialog();
}
}, [inputRef, noClick]);
const dragTargetsRef = useRef([]);
const onDocumentDrop = event => {
if (rootRef.current && rootRef.current.contains(event.target)) {
// If we intercepted an event for our instance, let it propagate down to the instance's onDrop handler
return;
}
event.preventDefault();
dragTargetsRef.current = [];
};
useEffect(() => {
if (preventDropOnDocument) {
document.addEventListener("dragover", onDocumentDragOver, false);
document.addEventListener("drop", onDocumentDrop, false);
}
return () => {
if (preventDropOnDocument) {
document.removeEventListener("dragover", onDocumentDragOver);
document.removeEventListener("drop", onDocumentDrop);
}
};
}, [rootRef, preventDropOnDocument]);
const onDragEnterCb = useCallback(
event => {
event.preventDefault(); // Persist here because we need the event later after getFilesFromEvent() is done
event.persist();
stopPropagation(event); // Count the dropzone and any children that are entered.
if (dragTargetsRef.current.indexOf(event.target) === -1) {
dragTargetsRef.current = [...dragTargetsRef.current, event.target];
}
if (isEvtWithFiles(event)) {
Promise.resolve(getFilesFromEvent(event)).then(draggedFiles => {
if (isPropagationStopped(event) && !noDragEventsBubbling) {
return;
}
dispatch({
draggedFiles,
isDragActive: true,
type: "setDraggedFiles"
});
if (onDragEnter) {
onDragEnter(event);
}
});
}
},
[getFilesFromEvent, onDragEnter, noDragEventsBubbling]
);
const onDragOverCb = useCallback(
event => {
event.preventDefault();
event.persist();
stopPropagation(event);
if (event.dataTransfer) {
try {
event.dataTransfer.dropEffect = "copy";
} catch {}
/* eslint-disable-line no-empty */
}
if (isEvtWithFiles(event) && onDragOver) {
onDragOver(event);
}
return false;
},
[onDragOver, noDragEventsBubbling]
);
const onDragLeaveCb = useCallback(
event => {
event.preventDefault();
event.persist();
stopPropagation(event); // Only deactivate once the dropzone and all children have been left
const targets = dragTargetsRef.current.filter(
target =>
target !== event.target &&
rootRef.current &&
rootRef.current.contains(target)
);
dragTargetsRef.current = targets;
if (targets.length > 0) {
return;
}
dispatch({
isDragActive: false,
type: "setDraggedFiles",
draggedFiles: []
});
if (isEvtWithFiles(event) && onDragLeave) {
onDragLeave(event);
}
},
[rootRef, onDragLeave, noDragEventsBubbling]
);
const onDropCb = useCallback(
event => {
event.preventDefault(); // Persist here because we need the event later after getFilesFromEvent() is done
event.persist();
stopPropagation(event);
dragTargetsRef.current = [];
dispatch({
type: "reset"
});
if (isEvtWithFiles(event)) {
Promise.resolve(getFilesFromEvent(event)).then(files => {
if (isPropagationStopped(event) && !noDragEventsBubbling) {
return;
}
const acceptedFiles = [];
const rejectedFiles = [];
files.forEach(file => {
if (
fileAccepted(file, accept) &&
fileMatchSize(file, maxSize, minSize)
) {
acceptedFiles.push(file);
} else {
rejectedFiles.push(file);
}
});
if (!multiple && acceptedFiles.length > 1) {
rejectedFiles.push(...acceptedFiles.splice(0)); // Reject everything and empty accepted files
}
dispatch({
acceptedFiles,
rejectedFiles,
type: "setFiles"
});
if (onDrop) {
onDrop(acceptedFiles, rejectedFiles, event);
}
if (rejectedFiles.length > 0 && onDropRejected) {
onDropRejected(rejectedFiles, event);
}
if (acceptedFiles.length > 0 && onDropAccepted) {
onDropAccepted(acceptedFiles, event);
}
});
}
},
[
multiple,
accept,
minSize,
maxSize,
getFilesFromEvent,
onDrop,
onDropAccepted,
onDropRejected,
noDragEventsBubbling
]
);
const composeHandler = fn => {
return disabled ? null : fn;
};
const composeKeyboardHandler = fn => {
return noKeyboard ? null : composeHandler(fn);
};
const composeDragHandler = fn => {
return noDrag ? null : composeHandler(fn);
};
const stopPropagation = event => {
if (noDragEventsBubbling) {
event.stopPropagation();
}
};
const getRootProps = useMemo(
() => ({
refKey = "ref",
onKeyDown,
onFocus,
onBlur,
onClick,
onDragEnter,
onDragOver,
onDragLeave,
onDrop,
...rest
} = {}) => ({
onKeyDown: composeKeyboardHandler(
composeEventHandlers(onKeyDown, onKeyDownCb)
),
onFocus: composeKeyboardHandler(composeEventHandlers(onFocus, onFocusCb)),
onBlur: composeKeyboardHandler(composeEventHandlers(onBlur, onBlurCb)),
onClick: composeHandler(composeEventHandlers(onClick, onClickCb)),
onDragEnter: composeDragHandler(
composeEventHandlers(onDragEnter, onDragEnterCb)
),
onDragOver: composeDragHandler(
composeEventHandlers(onDragOver, onDragOverCb)
),
onDragLeave: composeDragHandler(
composeEventHandlers(onDragLeave, onDragLeaveCb)
),
onDrop: composeDragHandler(composeEventHandlers(onDrop, onDropCb)),
[refKey]: rootRef,
...(!disabled && !noKeyboard
? {
tabIndex: 0
}
: {}),
...rest
}),
[
rootRef,
onKeyDownCb,
onFocusCb,
onBlurCb,
onClickCb,
onDragEnterCb,
onDragOverCb,
onDragLeaveCb,
onDropCb,
noKeyboard,
noDrag,
disabled
]
);
const onInputElementClick = useCallback(event => {
event.stopPropagation();
}, []);
const getInputProps = useMemo(
() => ({ refKey = "ref", onChange, onClick, ...rest } = {}) => {
const inputProps = {
accept,
multiple,
type: "file",
style: {
display: "none"
},
onChange: composeHandler(composeEventHandlers(onChange, onDropCb)),
onClick: composeHandler(
composeEventHandlers(onClick, onInputElementClick)
),
autoComplete: "off",
tabIndex: -1,
[refKey]: inputRef
};
return { ...inputProps, ...rest };
},
[inputRef, accept, multiple, onDropCb, disabled]
);
const fileCount = draggedFiles.length;
const isMultipleAllowed = multiple || fileCount <= 1;
const isDragAccept = fileCount > 0 && allFilesAccepted(draggedFiles, accept);
const isDragReject = fileCount > 0 && (!isDragAccept || !isMultipleAllowed);
return {
...state,
isDragAccept,
isDragReject,
isFocused: isFocused && !disabled,
getRootProps,
getInputProps,
rootRef,
inputRef,
open: composeHandler(openFileDialog)
};
}
function reducer(state, action) {
/* istanbul ignore next */
switch (action.type) {
case "focus":
return { ...state, isFocused: true };
case "blur":
return { ...state, isFocused: false };
case "openDialog":
return { ...state, isFileDialogActive: true };
case "closeDialog":
return { ...state, isFileDialogActive: false };
case "setDraggedFiles":
/* eslint no-case-declarations: 0 */
const { isDragActive, draggedFiles } = action;
return { ...state, draggedFiles, isDragActive };
case "setFiles":
return {
...state,
acceptedFiles: action.acceptedFiles,
rejectedFiles: action.rejectedFiles
};
case "reset":
return {
...state,
isFileDialogActive: false,
isDragActive: false,
draggedFiles: []
};
default:
return state;
}
}