From 6e415cfdef1858c3e2b2a768d9aef39da1f8fe14 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 27 Mar 2019 19:34:33 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20useDrop=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__stories__/useDrop.story.tsx | 47 +++++++++++++ src/index.ts | 2 + src/useDrop.ts | 112 ++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 src/__stories__/useDrop.story.tsx create mode 100644 src/useDrop.ts diff --git a/src/__stories__/useDrop.story.tsx b/src/__stories__/useDrop.story.tsx new file mode 100644 index 0000000000..78f0879e9a --- /dev/null +++ b/src/__stories__/useDrop.story.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import {action} from '@storybook/addon-actions'; +import {useDrop} from '..'; + +const Demo = () => { + const state = useDrop({ + onFiles: action('onFiles'), + onUri: action('onUri'), + onText: action('onText'), + }); + + const style: React.CSSProperties = { + width: 300, + height: 200, + margin: '50px auto', + border: '1px dotted #000', + textAlign: 'center', + lineHeight: '200px', + ...(state.over + ? { + border: '1px dotted green', + outline: '3px solid yellow', + background: '#f8f8f8', + } + : {}), + }; + + return ( +
+
Drop anywhere on page
+
+
    +
  • See logs in Actions tab.
  • +
  • Drag in and drop files.
  • +
  • Cmd + V paste text here.
  • +
  • Drag in images from other tabs.
  • +
  • Drag in link from navigation bar.
  • +
  • Below is state returned by the hook:
  • +
+
{JSON.stringify(state, null, 4)}
+
+
+ ); +}; + +storiesOf('UI|useDrop', module).add('Default', () => ); diff --git a/src/index.ts b/src/index.ts index 17425fd377..55a3844fe8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import useAsyncRetry from './useAsyncRetry'; import useAudio from './useAudio'; import useBattery from './useBattery'; import useBoolean from './useBoolean'; +import useDrop from './useDrop'; import useCounter from './useCounter'; import useCss from './useCss'; import useDebounce from './useDebounce'; @@ -64,6 +65,7 @@ export { useAudio, useBattery, useBoolean, + useDrop, useClickAway, useCounter, useCss, diff --git a/src/useDrop.ts b/src/useDrop.ts new file mode 100644 index 0000000000..352bcf8312 --- /dev/null +++ b/src/useDrop.ts @@ -0,0 +1,112 @@ +import * as React from 'react'; +import useRefMounted from './useRefMounted'; + +const {useState, useMemo, useCallback, useEffect} = React; + +export interface DropAreaState { + over: boolean; +} + +export interface DropAreaBond { + onDragOver: React.DragEventHandler; + onDragEnter: React.DragEventHandler; + onDragLeave: React.DragEventHandler; + onDrop: React.DragEventHandler; + onPaste: React.ClipboardEventHandler; +} + +export interface DropAreaOptions { + onFiles?: (files: File[], event?) => void; + onText?: (text: string, event?) => void; + onUri?: (url: string, event?) => void; +} + +const noop = () => {}; +const defaultState: DropAreaState = { + over: false, +}; + +const createProcess = (options: DropAreaOptions, mounted: React.RefObject) => ( + dataTransfer: DataTransfer, + event, +) => { + const uri = dataTransfer.getData('text/uri-list'); + + if (uri) { + (options.onUri || noop)(uri, event); + return; + } + + if (dataTransfer.files && dataTransfer.files.length) { + (options.onFiles || noop)(Array.from(dataTransfer.files), event); + return; + } + + if (dataTransfer.items && dataTransfer.items.length) { + dataTransfer.items[0].getAsString((text) => { + if (mounted.current) { + (options.onText || noop)(text, event); + } + }); + } +}; + +const useDrop = (options: DropAreaOptions = {}): DropAreaState => { + const {onFiles, onText, onUri} = options; + const mounted = useRefMounted(); + const [over, setOverRaw] = useState(false); + const setOver = useCallback(setOverRaw, []); + const process = useMemo(() => createProcess(options, mounted), [onFiles, onText, onUri]); + + useEffect(() => { + const onDragOver = (event) => { + event.preventDefault(); + }; + + const onDragEnter = (event) => { + event.preventDefault(); + setOver(true); + }; + + const onDragLeave = () => { + setOver(true); + }; + + const onDragExit = () => { + setOver(false); + }; + + const onDrop = (event) => { + event.preventDefault(); + setOver(false); + process(event.dataTransfer, event); + }; + + const onPaste = (event) => { + process(event.clipboardData, event); + }; + + window.addEventListener('dragover', onDragOver); + window.addEventListener('dragenter', onDragEnter); + window.addEventListener('dragleave', onDragLeave); + window.addEventListener('dragexit', onDragExit); + window.addEventListener('drop', onDrop); + + if (onText) { + window.addEventListener('paste', onPaste); + } + + return () => { + window.removeEventListener('dragover', onDragOver); + window.removeEventListener('dragenter', onDragEnter); + window.removeEventListener('dragleave', onDragLeave); + window.removeEventListener('dragexit', onDragExit); + window.removeEventListener('drop', onDrop); + window.removeEventListener('paste', onPaste); + }; + }, [process]); + + return {over}; +}; + +export default useDrop;