diff --git a/README.md b/README.md index be91072..94ce953 100644 --- a/README.md +++ b/README.md @@ -890,11 +890,7 @@ Latest version 4.0.2 (2022-01-26): * Fix onUploadAccepted signature when a preview is set -Version 4.0.1 (2022-01-21): - - * Fix config props does not work in CSVReader - -Version 4.0.0 (2022-01-18): +BREAKING CHANGE Version 4.0.0 (2022-01-18): * Improve code performance * Rewrite any existing based components to hooks diff --git a/src/ProgressBar.tsx b/src/ProgressBar.tsx index 1d13f05..817a4ad 100644 --- a/src/ProgressBar.tsx +++ b/src/ProgressBar.tsx @@ -9,21 +9,21 @@ const styles = { bottom: 14, width: '100%', // position: 'absolute', - } as CSSProperties, + }, button: { position: 'inherit', width: '100%', - } as CSSProperties, + }, fill: { backgroundColor: DEFAULT_PROGRESS_BAR_COLOR, borderRadius: 3, height: 10, transition: 'width 500ms ease-in-out', - } as CSSProperties, + }, }; interface Props { - style?: any; + style?: CSSProperties; className?: string; percentage: number; display: string; diff --git a/src/Remove.tsx b/src/Remove.tsx index 93e369e..6d2c640 100644 --- a/src/Remove.tsx +++ b/src/Remove.tsx @@ -1,12 +1,12 @@ import React from 'react'; -export interface Props { +export interface IRemove { color?: string; width?: number; height?: number; } -export default function Remove({ color, width = 23, height = 23 }: Props) { +export default function Remove({ color, width = 23, height = 23 }: IRemove) { return ( CSVDownloaderComponent, []) as any; + const CSVDownloader = useMemo(() => CSVDownloaderComponent, []); return CSVDownloader; } diff --git a/src/useCSVReader.tsx b/src/useCSVReader.tsx index 7a50154..ecf2ff7 100644 --- a/src/useCSVReader.tsx +++ b/src/useCSVReader.tsx @@ -6,6 +6,7 @@ import React, { useEffect, ReactNode, useRef, + CSSProperties, } from 'react'; import PapaParse, { ParseResult } from 'papaparse'; import { CustomConfig } from './model'; @@ -17,17 +18,13 @@ import { fileAccepted, fileMatchSize, TOO_MANY_FILES_REJECTION, + DEFAULT_ACCEPT, onDocumentDragOver, } from './utils'; import ProgressBar from './ProgressBar'; -import Remove, { Props as RemoveComponentProps } from './Remove'; +import Remove, { IRemove } from './Remove'; -// 'text/csv' for MacOS -// '.csv' for Linux -// 'application/vnd.ms-excel' for Window 10 -const DEFAULT_ACCEPT = 'text/csv, .csv, application/vnd.ms-excel'; - -export interface Props { +export interface ICSVReader { children: (fn: any) => void | ReactNode; accept?: string; config?: CustomConfig; @@ -48,13 +45,13 @@ export interface Props { ) => void; onUploadRejected?: (file?: File, event?: DragEvent | Event) => void; validator?: (file: File) => void; - onDragEnter?: (event?: DragEvent) => void; - onDragOver?: (event?: DragEvent) => void; - onDragLeave?: (event?: DragEvent) => void; + onDragEnter?: (event?: DragEvent | Event) => void; + onDragOver?: (event?: DragEvent | Event) => void; + onDragLeave?: (event?: DragEvent | Event) => void; } -export interface ProgressBarComponentProp { - style?: any; +export interface IProgressBar { + style?: CSSProperties; className?: string; } @@ -79,9 +76,9 @@ function useCSVReaderComponent() { onDragEnter, onDragOver, onDragLeave, - }: Props) => { - const inputRef: any = useRef(null); - const rootRef: any = useRef(null); + }: ICSVReader) => { + const inputRef = useRef(null); + const rootRef = useRef(null); const dragTargetsRef = useRef([]); const [state, dispatch] = useReducer(reducer, initialState); @@ -93,12 +90,15 @@ function useCSVReaderComponent() { isFileDialogActive, } = state; - const onDocumentDrop = (event: DragEvent) => { - if (rootRef.current && rootRef.current.contains(event.target)) { + const onDocumentDrop = (e: DragEvent) => { + // FIX: Type 'EventTarget' is not assignable to type 'Node' + // SOLUTION: event.target as Node + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239 + if (rootRef.current && rootRef.current.contains(e.target as Node)) { // If we intercepted an event for our instance, let it propagate down to the instance's onDrop handler return; } - event.preventDefault(); + e.preventDefault(); dragTargetsRef.current = []; }; @@ -116,7 +116,7 @@ function useCSVReaderComponent() { }; }, [rootRef, preventDropOnDocument]); - // == GLOBAL == + // -- GLOBAL -- const composeHandler = (fn: any) => { return disabled ? null : fn; }; @@ -125,17 +125,19 @@ function useCSVReaderComponent() { return noDrag ? null : composeHandler(fn); }; - const stopPropagation = (event: Event) => { + const stopPropagation = (e: Event) => { if (noDragEventsBubbling) { - event.stopPropagation(); + e.stopPropagation(); } }; - const allowDrop = (event: any) => { - event.preventDefault(event); + const allowDrop = (e: DragEvent) => { + e.preventDefault(); // Persist here because we need the event later after getFilesFromEvent() is done - event.persist(); - stopPropagation(event); + // Only for React 16 + // React 17 no event pooling + (e as any).persist(); + stopPropagation(e); }; const setDisplayProgressBar = (display: string) => { @@ -152,7 +154,7 @@ function useCSVReaderComponent() { }); }; - const ProgressBarComponent = (props: ProgressBarComponentProp) => { + const ProgressBarComponent = (props: IProgressBar) => { return ( () { ); }; - const RemoveComponent = (props: RemoveComponentProps) => { + const RemoveComponent = (props: IRemove) => { return ; }; @@ -180,7 +182,7 @@ function useCSVReaderComponent() { const openFileDialog = useCallback(() => { if (inputRef.current && state.displayProgressBar) { dispatch({ type: 'openDialog' }); - inputRef.current.value = null; + inputRef.current.value = ''; inputRef.current.click(); } }, [dispatch]); @@ -192,10 +194,7 @@ function useCSVReaderComponent() { setTimeout(() => { if (inputRef.current) { const { files } = inputRef.current; - - if (!files.length) { - dispatch({ type: 'closeDialog' }); - } + files && !files.length && dispatch({ type: 'closeDialog' }); } }, 300); } @@ -224,44 +223,48 @@ function useCSVReaderComponent() { }, [inputRef, noClick]); const onDropCb = useCallback( - (event: any) => { - allowDrop(event); + (e: DragEvent) => { + allowDrop(e); - setProgressBarPercentage(0); + // setProgressBarPercentage(0); dragTargetsRef.current = []; - if (isEventWithFiles(event)) { - if (isPropagationStopped(event) && !noDragEventsBubbling) { + if (isEventWithFiles(e)) { + if (isPropagationStopped(e) && !noDragEventsBubbling) { return; } - const acceptedFiles = [] as any; - const fileRejections = [] as any; + const acceptedFiles: any = []; + const fileRejections: any = []; + // FIX: event.target type (EventTarget) has no files + // https://github.com/microsoft/TypeScript/issues/31816 + const target = e.target as HTMLInputElement; const files = - event.target.files || - (event.dataTransfer && event.dataTransfer.files); - Array.from(files).forEach((file) => { - const [accepted, acceptError] = fileAccepted(file, accept); - const [sizeMatch, sizeError] = fileMatchSize( - file, - minSize, - maxSize, - ); - const customErrors = validator ? validator(file as File) : null; - - if (accepted && sizeMatch && !customErrors) { - acceptedFiles.push(file); - } else { - let errors = [acceptError, sizeError]; - - if (customErrors) { - errors = errors.concat(customErrors); + target.files || (e.dataTransfer && e.dataTransfer.files); + if (files) { + Array.from(files).forEach((file) => { + const [accepted, acceptError] = fileAccepted(file, accept); + const [sizeMatch, sizeError] = fileMatchSize( + file, + minSize, + maxSize, + ); + const customErrors = validator ? validator(file as File) : null; + + if (accepted && sizeMatch && !customErrors) { + acceptedFiles.push(file); + } else { + let errors = [acceptError, sizeError]; + + if (customErrors) { + errors = errors.concat(customErrors); + } + + fileRejections.push({ file, errors: errors.filter((e) => e) }); } - - fileRejections.push({ file, errors: errors.filter((e) => e) }); - } - }); + }); + } if ( (!multiple && acceptedFiles.length > 1) || @@ -287,11 +290,11 @@ function useCSVReaderComponent() { // } if (fileRejections.length > 0 && onUploadRejected) { - onUploadRejected(fileRejections, event); + onUploadRejected(fileRejections, e); } if (acceptedFiles.length > 0 && onUploadAccepted) { - let configs = {} as any; + let configs: any = {}; const data: any = []; const errors: any = []; const meta: any = []; @@ -326,7 +329,7 @@ function useCSVReaderComponent() { percentage = Math.round( (data.length / config.preview) * 100, ); - // setProgressBarPercentage(percentage); + setProgressBarPercentage(percentage); if (data.length === config.preview) { const obj = { data, errors, meta }; onUploadAccepted(obj, file); @@ -340,8 +343,8 @@ function useCSVReaderComponent() { return; } percentage = newPercentage; + setProgressBarPercentage(percentage); } - setProgressBarPercentage(percentage); }, }; configs = Object.assign({}, config, configs); @@ -352,7 +355,7 @@ function useCSVReaderComponent() { reader.onloadend = () => { setTimeout(() => { setDisplayProgressBar('none'); - }, 2000); + }, 1000); }; reader.readAsText(file, config.encoding || 'utf-8'); }); @@ -371,27 +374,27 @@ function useCSVReaderComponent() { ], ); - const onInputElementClick = useCallback((event) => { - stopPropagation(event); + const onInputElementClick = useCallback((e: Event) => { + stopPropagation(e); }, []); - // ============ + // ------------ - // == BUTTON | DROP == + // -- BUTTON | DROP -- const composeKeyboardHandler = (fn: any) => { return noKeyboard ? null : composeHandler(fn); }; const onDragEnterCb = useCallback( - (event: DragEvent) => { - allowDrop(event); + (e: DragEvent) => { + allowDrop(e); dragTargetsRef.current = [ ...dragTargetsRef.current, - event.target, + e.target, ] as never[]; - if (isEventWithFiles(event)) { - if (isPropagationStopped(event) && !noDragEventsBubbling) { + if (isEventWithFiles(e)) { + if (isPropagationStopped(e) && !noDragEventsBubbling) { return; } @@ -402,7 +405,7 @@ function useCSVReaderComponent() { }); if (onDragEnter) { - onDragEnter(event); + onDragEnter(e); } } }, @@ -410,18 +413,18 @@ function useCSVReaderComponent() { ); const onDragOverCb = useCallback( - (event: DragEvent) => { - allowDrop(event); + (e: DragEvent) => { + allowDrop(e); - const hasFiles = isEventWithFiles(event); - if (hasFiles && event.dataTransfer) { + const hasFiles = isEventWithFiles(e); + if (hasFiles && e.dataTransfer) { try { - event.dataTransfer.dropEffect = 'copy'; + e.dataTransfer.dropEffect = 'copy'; } catch {} } if (hasFiles && onDragOver) { - onDragOver(event); + onDragOver(e); } return false; @@ -430,8 +433,8 @@ function useCSVReaderComponent() { ); const onDragLeaveCb = useCallback( - (event: DragEvent) => { - allowDrop(event); + (e: DragEvent) => { + allowDrop(e); // Only deactivate once the dropzone and all children have been left const targets = dragTargetsRef.current.filter( @@ -439,7 +442,7 @@ function useCSVReaderComponent() { ); // Make sure to remove a target present multiple times only once // (Firefox may fire dragenter/dragleave multiple times on the same element) - const targetIdx = targets.indexOf(event.target as never); + const targetIdx = targets.indexOf(e.target as never); if (targetIdx !== -1) { targets.splice(targetIdx, 1); } @@ -454,8 +457,8 @@ function useCSVReaderComponent() { draggedFiles: [], }); - if (isEventWithFiles(event) && onDragLeave) { - onDragLeave(event); + if (isEventWithFiles(e) && onDragLeave) { + onDragLeave(e); } }, [rootRef, onDragLeave, noDragEventsBubbling], @@ -463,14 +466,17 @@ function useCSVReaderComponent() { // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone const onKeyDownCb = useCallback( - (event: KeyboardEvent) => { + (e: KeyboardEvent) => { // Ignore keyboard events bubbling up the DOM tree - if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) { + if ( + !rootRef.current || + !rootRef.current.isEqualNode(e.target as Node) + ) { return; } - if (event.key === 'Space' || event.key === 'Enter') { - event.preventDefault(); + if (e.key === 'Space' || e.key === 'Enter') { + e.preventDefault(); openFileDialog(); } }, @@ -538,9 +544,9 @@ function useCSVReaderComponent() { disabled, ], ); - // =================== + // ------------------- - // == INPUT == + // -- INPUT PROPS -- const getInputProps = useMemo( () => ({ @@ -570,13 +576,16 @@ function useCSVReaderComponent() { }, [inputRef, accept, onDropCb, disabled], ); - // =========== - - const removeFileProgrammaticallyCb = useCallback((event: Event) => { - inputRef.current.value = ''; - dispatch({ type: 'reset' }); - // To prevent a parents onclick event from firing when a child is clicked - event.stopPropagation(); + // ----------------- + + // -- REMOVE BUTTON -- + const removeFileProgrammaticallyCb = useCallback((e: Event) => { + if (inputRef.current) { + inputRef.current.value = ''; + dispatch({ type: 'reset' }); + // To prevent a parents onclick event from firing when a child is clicked + e.stopPropagation(); + } }, []); const getRemoveFileProps = useMemo( @@ -589,6 +598,7 @@ function useCSVReaderComponent() { }), [removeFileProgrammaticallyCb], ); + // ------------------- return ( <> @@ -598,7 +608,7 @@ function useCSVReaderComponent() { ); }; - const CSVReader = useMemo(() => CSVReaderComponent, []) as any; + const CSVReader = useMemo(() => CSVReaderComponent, []); return CSVReader; } diff --git a/src/utils.ts b/src/utils.ts index 086c6ab..885b94b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,8 @@ +// 'text/csv' for MacOS +// '.csv' for Linux +// 'application/vnd.ms-excel' for Window 10 +export const DEFAULT_ACCEPT = 'text/csv, .csv, application/vnd.ms-excel'; + // Error codes export const FILE_INVALID_TYPE = 'file-invalid-type'; export const FILE_TOO_LARGE = 'file-too-large'; @@ -56,13 +61,13 @@ export function lightenDarkenColor(col: string, amt: number) { return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16); } -function isIe(userAgent: any) { +function isIe(userAgent: string) { return ( userAgent.indexOf('MSIE') !== -1 || userAgent.indexOf('Trident/') !== -1 ); } -function isEdge(userAgent: any) { +function isEdge(userAgent: string) { return userAgent.indexOf('Edge/') !== -1; } @@ -73,11 +78,11 @@ export function isIeOrEdge(userAgent = window.navigator.userAgent) { // React's synthetic events has event.isPropagationStopped, // but to remain compatibility with other libs (Preact) fall back // to check event.cancelBubble -export function isPropagationStopped(event: any) { - if (typeof event.isPropagationStopped === 'function') { - return event.isPropagationStopped(); - } else if (typeof event.cancelBubble !== 'undefined') { - return event.cancelBubble; +export function isPropagationStopped(e: any) { + if (typeof e.isPropagationStopped === 'function') { + return e.isPropagationStopped(); + } else if (typeof e.cancelBubble !== 'undefined') { + return e.cancelBubble; } return false; } @@ -93,23 +98,24 @@ export function isPropagationStopped(event: any) { * @return {Function} the event handler to add to an element */ export function composeEventHandlers(...fns: any[]) { - return (event: any, ...args: any[]) => + return (e: Event, ...args: any[]) => fns.some((fn) => { - if (!isPropagationStopped(event) && fn) { - fn(event, ...args); + if (!isPropagationStopped(e) && fn) { + fn(e, ...args); } - return isPropagationStopped(event); + return isPropagationStopped(e); }); } -export function isEventWithFiles(event: any) { - if (!event.dataTransfer) { - return !!event.target && !!event.target.files; +export function isEventWithFiles(e: DragEvent) { + if (!e.dataTransfer) { + const target = e.target as HTMLInputElement; + return !!e.target && !!target.files; } // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file return Array.prototype.some.call( - event.dataTransfer.types, + e.dataTransfer.types, (type) => type === 'Files' || type === 'application/x-moz-file', ); } @@ -125,7 +131,7 @@ export function isEventWithFiles(event: any) { * @returns {boolean} */ -export function accepts(file: any, acceptedFiles: any) { +export function accepts(file: File, acceptedFiles: string) { if (file && acceptedFiles) { const acceptedFilesArray = Array.isArray(acceptedFiles) ? acceptedFiles @@ -134,7 +140,7 @@ export function accepts(file: any, acceptedFiles: any) { const mimeType = (file.type || '').toLowerCase(); const baseMimeType = mimeType.replace(/\/.*$/, ''); - return acceptedFilesArray.some((type: any) => { + return acceptedFilesArray.some((type: string) => { const validType = type.trim().toLowerCase(); if (validType.charAt(0) === '.') { return fileName.toLowerCase().endsWith(validType); @@ -149,7 +155,7 @@ export function accepts(file: any, acceptedFiles: any) { } // File Errors -export const getInvalidTypeRejectionErr = (accept: any) => { +export const getInvalidTypeRejectionErr = (accept: string) => { accept = Array.isArray(accept) && accept.length === 1 ? accept[0] : accept; const messageSuffix = Array.isArray(accept) ? `one of ${accept.join(', ')}` @@ -162,7 +168,7 @@ export const getInvalidTypeRejectionErr = (accept: any) => { // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with // that MIME type will always be accepted -export function fileAccepted(file: any, accept: any) { +export function fileAccepted(file: File, accept: string) { const isAcceptable = file.type === 'application/x-moz-file' || accepts(file, accept); return [ @@ -171,7 +177,7 @@ export function fileAccepted(file: any, accept: any) { ]; } -export function fileMatchSize(file: any, minSize: any, maxSize: any) { +export function fileMatchSize(file: File, minSize: number, maxSize: number) { if (isDefined(file.size)) { if (isDefined(minSize) && isDefined(maxSize)) { if (file.size > maxSize) { @@ -189,18 +195,18 @@ export function fileMatchSize(file: any, minSize: any, maxSize: any) { return [true, null]; } -function isDefined(value: any) { +function isDefined(value: number) { return value !== undefined && value !== null; } -export const getTooLargeRejectionErr = (maxSize: any) => { +export const getTooLargeRejectionErr = (maxSize: number) => { return { code: FILE_TOO_LARGE, message: `File is larger than ${maxSize} bytes`, }; }; -export const getTooSmallRejectionErr = (minSize: any) => { +export const getTooSmallRejectionErr = (minSize: number) => { return { code: FILE_TOO_SMALL, message: `File is smaller than ${minSize} bytes`, @@ -213,35 +219,34 @@ export const TOO_MANY_FILES_REJECTION = { }; // allow the entire document to be a drag target -export function onDocumentDragOver(event: any) { - event.preventDefault(); +export function onDocumentDragOver(e: DragEvent) { + e.preventDefault(); } -interface Params { +interface IAllFilesAccepted { files?: any; - accept?: any; + accept?: string; minSize?: number; maxSize?: number; - multiple?: any; - maxFiles?: any; + multiple?: boolean; + maxFiles?: number; } export function allFilesAccepted({ files, - accept, - minSize, - maxSize, + accept = DEFAULT_ACCEPT, + minSize = 1, + maxSize = 1, multiple, maxFiles, -}: Params) { +}: IAllFilesAccepted) { if ( (!multiple && files.length > 1) || - (multiple && maxFiles >= 1 && files.length > maxFiles) + (multiple && maxFiles && maxFiles >= 1 && files.length > maxFiles) ) { return false; } - - return files.every((file: any) => { + return files.every((file: File) => { const [accepted] = fileAccepted(file, accept); const [sizeMatch] = fileMatchSize(file, minSize, maxSize); return accepted && sizeMatch; diff --git a/supports/create-next-app/pages/index.tsx b/supports/create-next-app/pages/index.tsx index 68ac5d7..bc527ed 100644 --- a/supports/create-next-app/pages/index.tsx +++ b/supports/create-next-app/pages/index.tsx @@ -1,19 +1,158 @@ -import React from 'react'; +import React, { useState, CSSProperties } from 'react'; -import { usePapaParse } from 'react-papaparse'; +import { + useCSVReader, + lightenDarkenColor, + formatFileSize, +} from 'react-papaparse'; -export default function ReadRemoteFile() { - const { readRemoteFile } = usePapaParse(); +const GREY = '#CCC'; +const GREY_LIGHT = 'rgba(255, 255, 255, 0.4)'; +const DEFAULT_REMOVE_HOVER_COLOR = '#A01919'; +const REMOVE_HOVER_COLOR_LIGHT = lightenDarkenColor( + DEFAULT_REMOVE_HOVER_COLOR, + 40 +); +const GREY_DIM = '#686868'; - const handleReadRemoteFile = () => { - readRemoteFile('https://react-papaparse.js.org/static/csv/normal.csv', { - complete: (results) => { +const styles = { + zone: { + alignItems: 'center', + border: `2px dashed ${GREY}`, + borderRadius: 20, + display: 'flex', + flexDirection: 'column', + height: '100%', + justifyContent: 'center', + padding: 20, + } as CSSProperties, + file: { + background: 'linear-gradient(to bottom, #EEE, #DDD)', + borderRadius: 20, + display: 'flex', + height: 120, + width: 120, + position: 'relative', + zIndex: 10, + flexDirection: 'column', + justifyContent: 'center', + } as CSSProperties, + info: { + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + paddingLeft: 10, + paddingRight: 10, + } as CSSProperties, + size: { + backgroundColor: GREY_LIGHT, + borderRadius: 3, + marginBottom: '0.5em', + justifyContent: 'center', + display: 'flex', + } as CSSProperties, + name: { + backgroundColor: GREY_LIGHT, + borderRadius: 3, + fontSize: 12, + marginBottom: '0.5em', + } as CSSProperties, + progressBar: { + bottom: 14, + position: 'absolute', + width: '100%', + paddingLeft: 10, + paddingRight: 10, + } as CSSProperties, + zoneHover: { + borderColor: GREY_DIM, + } as CSSProperties, + default: { + borderColor: GREY, + } as CSSProperties, + remove: { + height: 23, + position: 'absolute', + right: 6, + top: 6, + width: 23, + } as CSSProperties, +}; + +export default function CSVReader() { + const { CSVReader } = useCSVReader(); + const [zoneHover, setZoneHover] = useState(false); + const [removeHoverColor, setRemoveHoverColor] = useState( + DEFAULT_REMOVE_HOVER_COLOR + ); + + return ( + { console.log('---------------------------'); - console.log('Results:', results); + console.log(results); console.log('---------------------------'); - }, - }); - }; - - return ; + setZoneHover(false); + }} + onDragOver={(event: DragEvent) => { + event.preventDefault(); + setZoneHover(true); + }} + onDragLeave={(event: DragEvent) => { + event.preventDefault(); + setZoneHover(false); + }} + > + {({ + getRootProps, + acceptedFile, + ProgressBar, + getRemoveFileProps, + Remove, + }: any) => ( + <> +
+ {acceptedFile ? ( + <> +
+
+ + {formatFileSize(acceptedFile.size)} + + {acceptedFile.name} +
+
+ +
+
{ + event.preventDefault(); + setRemoveHoverColor(REMOVE_HOVER_COLOR_LIGHT); + }} + onMouseOut={(event: Event) => { + event.preventDefault(); + setRemoveHoverColor(DEFAULT_REMOVE_HOVER_COLOR); + }} + > + +
+
+ + ) : ( + 'Drop CSV file here or click to upload' + )} +
+ + )} + + ); }