diff --git a/docs/demo/paste.md b/docs/demo/paste.md new file mode 100644 index 0000000..51574df --- /dev/null +++ b/docs/demo/paste.md @@ -0,0 +1,8 @@ +--- +title: paste +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/paste.tsx b/docs/examples/paste.tsx new file mode 100644 index 0000000..05e41f0 --- /dev/null +++ b/docs/examples/paste.tsx @@ -0,0 +1,43 @@ +/* eslint no-console:0 */ +import React from 'react'; +import Upload from 'rc-upload'; + +const props = { + action: '/upload.do', + type: 'drag', + accept: '.png', + beforeUpload(file) { + console.log('beforeUpload', file.name); + }, + onStart: file => { + console.log('onStart', file.name); + }, + onSuccess(file) { + console.log('onSuccess', file); + }, + onProgress(step, file) { + console.log('onProgress', Math.round(step.percent), file.name); + }, + onError(err) { + console.log('onError', err); + }, + style: { display: 'inline-block', width: 200, height: 200, background: '#eee' }, +}; + +const Test = () => { + return ( +
+
+ + 开始上传 + +
+
+ ); +}; + +export default Test; diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index e88c291..155384f 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -28,6 +28,8 @@ class AjaxUploader extends Component { private fileInput: HTMLInputElement; + private isMouseEnter: boolean; + private _isMounted: boolean; onChange = (e: React.ChangeEvent) => { @@ -66,41 +68,57 @@ class AjaxUploader extends Component { } }; - onFileDrop = (e: React.DragEvent) => { - const { multiple } = this.props; - + onFileDropOrPaste = (e: React.DragEvent | ClipboardEvent) => { e.preventDefault(); if (e.type === 'dragover') { return; } - if (this.props.directory) { - traverseFileTree( - Array.prototype.slice.call(e.dataTransfer.items), - this.uploadFiles, - (_file: RcFile) => attrAccept(_file, this.props.accept), + const { multiple, accept, directory } = this.props; + let items: DataTransferItem[] = []; + let files: File[] = []; + + if (e.type === 'drop') { + const dataTransfer = (e as React.DragEvent).dataTransfer; + items = [...(dataTransfer.items || [])]; + files = [...(dataTransfer.files || [])]; + } else if (e.type === 'paste') { + const clipboardData = (e as ClipboardEvent).clipboardData; + items = [...(clipboardData.items || [])]; + files = [...(clipboardData.files || [])]; + } + + if (directory) { + traverseFileTree(Array.prototype.slice.call(items), this.uploadFiles, (_file: RcFile) => + attrAccept(_file, accept), ); } else { - let files = [...e.dataTransfer.files].filter((file: RcFile) => - attrAccept(file, this.props.accept), - ); + let acceptFiles = [...files].filter((file: RcFile) => attrAccept(file, accept)); if (multiple === false) { - files = files.slice(0, 1); + acceptFiles = files.slice(0, 1); } - this.uploadFiles(files); + this.uploadFiles(acceptFiles); } }; + onPrePaste(e: ClipboardEvent) { + if (this.isMouseEnter) { + this.onFileDropOrPaste(e); + } + } + componentDidMount() { this._isMounted = true; + document.addEventListener('paste', this.onPrePaste.bind(this)); } componentWillUnmount() { this._isMounted = false; this.abort(); + document.removeEventListener('paste', this.onPrePaste.bind(this)); } uploadFiles = (files: File[]) => { @@ -261,6 +279,18 @@ class AjaxUploader extends Component { this.fileInput = node; }; + handleMouseEnter = (e: React.MouseEvent) => { + this.isMouseEnter = true; + + this.props.onMouseEnter?.(e); + }; + + handleMouseLeave = (e: React.MouseEvent) => { + this.isMouseEnter = false; + + this.props.onMouseLeave?.(e); + }; + render() { const { component: Tag, @@ -277,8 +307,6 @@ class AjaxUploader extends Component { children, directory, openFileDialogOnClick, - onMouseEnter, - onMouseLeave, hasControlInside, ...otherProps } = this.props; @@ -296,10 +324,10 @@ class AjaxUploader extends Component { : { onClick: openFileDialogOnClick ? this.onClick : () => {}, onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => {}, - onMouseEnter, - onMouseLeave, - onDrop: this.onFileDrop, - onDragOver: this.onFileDrop, + onMouseEnter: this.handleMouseEnter, + onMouseLeave: this.handleMouseLeave, + onDrop: this.onFileDropOrPaste, + onDragOver: this.onFileDropOrPaste, tabIndex: hasControlInside ? undefined : '0', }; return ( diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index b7e060c..4ac119f 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -306,6 +306,106 @@ describe('uploader', () => { }, 100); }); + it('paste to upload', done => { + const rcUpload = uploader.container.querySelector('.rc-upload')!; + const input = uploader.container.querySelector('input')!; + + const files = [ + { + name: 'success.png', + toString() { + return this.name; + }, + }, + ]; + (files as any).item = (i: number) => files[i]; + + handlers.onSuccess = (ret, file) => { + expect(ret[1]).toEqual(file.name); + expect(file).toHaveProperty('uid'); + done(); + }; + + handlers.onError = err => { + done(err); + }; + + fireEvent.mouseEnter(rcUpload); + fireEvent.paste(input, { + clipboardData: { files }, + }); + + setTimeout(() => { + requests[0].respond(200, {}, `["","${files[0].name}"]`); + }, 100); + }); + + it('paste unaccepted type files to upload will not trigger onStart', done => { + const input = uploader.container.querySelector('input')!; + const files = [ + { + name: 'success.jpg', + toString() { + return this.name; + }, + }, + ]; + (files as any).item = (i: number) => files[i]; + + fireEvent.paste(input, { + clipboardData: { files }, + }); + const mockStart = jest.fn(); + handlers.onStart = mockStart; + setTimeout(() => { + expect(mockStart.mock.calls.length).toBe(0); + done(); + }, 100); + }); + + it('paste files with multiple false', done => { + const { container } = render(); + const rcUpload = container.querySelector('.rc-upload')!; + const input = container.querySelector('input')!; + const files = [ + new File([''], 'success.png', { type: 'image/png' }), + new File([''], 'filtered.png', { type: 'image/png' }), + ]; + Object.defineProperty(files, 'item', { + value: i => files[i], + }); + + // Only can trigger once + let triggerTimes = 0; + handlers.onStart = () => { + triggerTimes += 1; + }; + handlers.onSuccess = (ret, file) => { + try { + expect(ret[1]).toEqual(file.name); + expect(file).toHaveProperty('uid'); + expect(triggerTimes).toEqual(1); + done(); + } catch (error) { + done(error); + } + }; + handlers.onError = error => { + done(error); + }; + + Object.defineProperty(input, 'files', { + value: files, + }); + + fireEvent.mouseEnter(rcUpload); + fireEvent.paste(input, { clipboardData: { files } }); + + setTimeout(() => { + handlers.onSuccess!(['', files[0].name] as any, files[0] as any, null!); + }, 100); + }); + it('support action and data is function returns Promise', async () => { const action: any = () => { return new Promise(resolve => { @@ -332,6 +432,21 @@ describe('uploader', () => { await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 2000)); }); + + it('support onMouseEnter and onMouseLeave', async () => { + const onMouseEnter = jest.fn(); + const onMouseLeave = jest.fn(); + + const { container } = render( + , + ); + const rcUpload = container.querySelector('.rc-upload')!; + + fireEvent.mouseEnter(rcUpload); + fireEvent.mouseLeave(rcUpload); + expect(onMouseEnter).toHaveBeenCalled(); + expect(onMouseLeave).toHaveBeenCalled(); + }); }); describe('directory uploader', () => {