UNPKG

react-native-surveys

Version:

Build your own forms, surveys and polls for your React Native apps.

484 lines (434 loc) 12.5 kB
/* 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; } }