From acde511a6171651bd5c9665789dbd3764b111cdb Mon Sep 17 00:00:00 2001 From: madocto Date: Sat, 20 Jan 2024 00:32:49 +0800 Subject: [PATCH 1/4] feat: support paste upload file --- src/AjaxUploader.tsx | 41 +++++++++++------- tests/uploader.spec.tsx | 96 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index e88c291..aaeb4c4 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -66,31 +66,41 @@ class AjaxUploader extends Component { } }; - onFileDrop = (e: React.DragEvent) => { - const { multiple } = this.props; - + onFileDropOrPaste = ( + e: React.DragEvent | React.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 React.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); } }; @@ -298,8 +308,9 @@ class AjaxUploader extends Component { onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => {}, onMouseEnter, onMouseLeave, - onDrop: this.onFileDrop, - onDragOver: this.onFileDrop, + onDrop: this.onFileDropOrPaste, + onDragOver: this.onFileDropOrPaste, + onPaste: this.onFileDropOrPaste, tabIndex: hasControlInside ? undefined : '0', }; return ( diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index b7e060c..6917599 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -306,6 +306,102 @@ describe('uploader', () => { }, 100); }); + it('paste to upload', done => { + 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.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 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.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 => { From bc718b0f510f1150b951d14e296eb194fb21791b Mon Sep 17 00:00:00 2001 From: madocto Date: Sat, 20 Jan 2024 18:25:27 +0800 Subject: [PATCH 2/4] feat: listen document paste event --- src/AjaxUploader.tsx | 35 ++++++++++++++++++++++++++--------- tests/uploader.spec.tsx | 4 ++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index aaeb4c4..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,9 +68,7 @@ class AjaxUploader extends Component { } }; - onFileDropOrPaste = ( - e: React.DragEvent | React.ClipboardEvent, - ) => { + onFileDropOrPaste = (e: React.DragEvent | ClipboardEvent) => { e.preventDefault(); if (e.type === 'dragover') { @@ -84,7 +84,7 @@ class AjaxUploader extends Component { items = [...(dataTransfer.items || [])]; files = [...(dataTransfer.files || [])]; } else if (e.type === 'paste') { - const clipboardData = (e as React.ClipboardEvent).clipboardData; + const clipboardData = (e as ClipboardEvent).clipboardData; items = [...(clipboardData.items || [])]; files = [...(clipboardData.files || [])]; } @@ -104,13 +104,21 @@ class AjaxUploader extends Component { } }; + 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[]) => { @@ -271,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, @@ -287,8 +307,6 @@ class AjaxUploader extends Component { children, directory, openFileDialogOnClick, - onMouseEnter, - onMouseLeave, hasControlInside, ...otherProps } = this.props; @@ -306,11 +324,10 @@ class AjaxUploader extends Component { : { onClick: openFileDialogOnClick ? this.onClick : () => {}, onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => {}, - onMouseEnter, - onMouseLeave, + onMouseEnter: this.handleMouseEnter, + onMouseLeave: this.handleMouseLeave, onDrop: this.onFileDropOrPaste, onDragOver: this.onFileDropOrPaste, - onPaste: this.onFileDropOrPaste, tabIndex: hasControlInside ? undefined : '0', }; return ( diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index 6917599..2fd2477 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -307,6 +307,7 @@ describe('uploader', () => { }); it('paste to upload', done => { + const rcUpload = uploader.container.querySelector('.rc-upload')!; const input = uploader.container.querySelector('input')!; const files = [ @@ -329,6 +330,7 @@ describe('uploader', () => { done(err); }; + fireEvent.mouseEnter(rcUpload); fireEvent.paste(input, { clipboardData: { files }, }); @@ -363,6 +365,7 @@ describe('uploader', () => { 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' }), @@ -395,6 +398,7 @@ describe('uploader', () => { value: files, }); + fireEvent.mouseEnter(rcUpload); fireEvent.paste(input, { clipboardData: { files } }); setTimeout(() => { From 216baf70e711f4f6f2ec08ca3fa1caf017591db9 Mon Sep 17 00:00:00 2001 From: madocto Date: Sat, 20 Jan 2024 20:30:48 +0800 Subject: [PATCH 3/4] docs: add demo --- docs/demo/paste.md | 8 ++++++++ docs/examples/paste.tsx | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 docs/demo/paste.md create mode 100644 docs/examples/paste.tsx 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; From ffdf33fee5205eb19e78395de983e72651ab5f19 Mon Sep 17 00:00:00 2001 From: madocto Date: Sun, 21 Jan 2024 00:19:45 +0800 Subject: [PATCH 4/4] test: add test --- tests/uploader.spec.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index 2fd2477..4ac119f 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -432,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', () => {