Skip to content

Commit b741c74

Browse files
committed
feat: 增加浏览器中粘贴上传的功能,并增加相应的演示功能
1 parent a0e26ee commit b741c74

File tree

6 files changed

+170
-4
lines changed

6 files changed

+170
-4
lines changed

Diff for: src/packages/uploader/__tests__/uploader.spec.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,46 @@ test('preview component', () => {
286286
)
287287
expect(clickFunc).toBeCalled()
288288
})
289+
290+
test('should handle paste upload', async () => {
291+
// arrange
292+
const onChange = vi.fn()
293+
294+
const { container } = render(
295+
<Uploader onChange={onChange} enablePasteUpload autoUpload={false} />
296+
)
297+
298+
const file = new File(['image data'], 'pasted-image.png', {
299+
type: 'image/png',
300+
})
301+
302+
const pasteEvent = new ClipboardEvent('paste', {
303+
bubbles: true,
304+
cancelable: true,
305+
clipboardData: new DataTransfer(),
306+
})
307+
308+
pasteEvent.clipboardData?.items.add(file)
309+
310+
// act
311+
await import('@testing-library/react').then(({ act: testAct }) =>
312+
testAct(async () => {
313+
container.firstChild?.dispatchEvent(pasteEvent)
314+
})
315+
)
316+
317+
// assert
318+
expect(onChange).toHaveBeenCalled()
319+
320+
const lastCallArgs = onChange.mock.calls[onChange.mock.calls.length - 1][0]
321+
322+
expect(lastCallArgs).toEqual(
323+
expect.arrayContaining([
324+
expect.objectContaining({
325+
name: 'pasted-image.png',
326+
type: 'image/png',
327+
status: 'ready',
328+
}),
329+
])
330+
)
331+
})

Diff for: src/packages/uploader/demo.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Demo11 from './demos/h5/demo11'
1515
import Demo12 from './demos/h5/demo12'
1616
import Demo13 from './demos/h5/demo13'
1717
import Demo14 from './demos/h5/demo14'
18+
import Demo15 from './demos/h5/demo15'
1819

1920
const UploaderDemo = () => {
2021
const [translated] = useTranslate({
@@ -32,6 +33,7 @@ const UploaderDemo = () => {
3233
manualExecution: '选中文件后,通过按钮手动执行上传',
3334
disabled: '禁用状态',
3435
customDeleteIcon: '自定义删除icon',
36+
enablePasteUpload: '启用粘贴上传',
3537
},
3638
'zh-TW': {
3739
basic: '基础用法',
@@ -47,6 +49,7 @@ const UploaderDemo = () => {
4749
manualExecution: '選取檔後,通過按鈕手動執行上傳',
4850
disabled: '禁用狀態',
4951
customDeleteIcon: '自定義刪除icon',
52+
enablePasteUpload: '啟用粘貼上傳',
5053
},
5154
'en-US': {
5255
basic: 'Basic usage',
@@ -63,6 +66,7 @@ const UploaderDemo = () => {
6366
'After selecting Chinese, manually perform the upload via the button',
6467
disabled: 'Disabled state',
6568
customDeleteIcon: 'Custom DeleteIcon',
69+
enablePasteUpload: 'Enable paste upload',
6670
},
6771
})
6872

@@ -97,6 +101,8 @@ const UploaderDemo = () => {
97101
<Demo13 />
98102
<h2>{translated.customDeleteIcon}</h2>
99103
<Demo14 />
104+
<h2>{translated.enablePasteUpload}</h2>
105+
<Demo15 />
100106
</div>
101107
</>
102108
)

Diff for: src/packages/uploader/demos/h5/demo15.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react'
2+
import { Uploader } from '@nutui/nutui-react'
3+
4+
const Demo15 = () => {
5+
return (
6+
<>
7+
<Uploader enablePasteUpload />
8+
</>
9+
)
10+
}
11+
export default Demo15

Diff for: src/packages/uploader/doc.md

+9
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ app.post('/upload', upload.single('file'), (req, res) => {
137137

138138
:::
139139

140+
### 浏览器中粘贴图片上传
141+
142+
:::demo
143+
144+
<CodeBlock src='h5/demo15.tsx'></CodeBlock>
145+
146+
:::
147+
140148
## Uploader
141149

142150
### Props
@@ -193,6 +201,7 @@ app.post('/upload', upload.single('file'), (req, res) => {
193201
| url | 文件路径 | `-` |
194202
| type | 文件类型 | `image/jpeg` |
195203
| formData | 上传所需的data | `new FormData()` |
204+
| enablePasteUpload | 是否支持粘贴上传 | `false` |
196205

197206
### Methods
198207

Diff for: src/packages/uploader/uploader.taro.tsx

+17-3
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ const InternalUploader: ForwardRefRenderFunction<
418418
for (const [key, value] of Object.entries(data)) {
419419
formData.append(key, value as any)
420420
}
421-
formData.append(name, file.originalFileObj as Blob)
421+
formData.append(name, (file.originalFileObj as Blob) ?? file)
422422
fileItem.name = file.originalFileObj?.name
423423
fileItem.type = file.originalFileObj?.type
424424
fileItem.formData = formData
@@ -428,8 +428,22 @@ const InternalUploader: ForwardRefRenderFunction<
428428
if (preview) {
429429
fileItem.url = fileType === 'video' ? file.thumbTempFilePath : filepath
430430
}
431-
executeUpload(fileItem, index)
432-
results.push(fileItem)
431+
if (preview && file.type?.includes('image')) {
432+
const reader = new FileReader()
433+
reader.onload = (event) => {
434+
fileItem.url = event.target?.result as string
435+
fileItem.path = event.target?.result as string
436+
fileItem.name = (file as unknown as Blob).name
437+
executeUpload(fileItem, index)
438+
results.push(fileItem)
439+
setFileList([...fileList, ...results])
440+
}
441+
442+
reader.readAsDataURL(file as unknown as Blob)
443+
} else {
444+
executeUpload(fileItem, index)
445+
results.push(fileItem)
446+
}
433447
})
434448
setFileList([...fileList, ...results])
435449
}

Diff for: src/packages/uploader/uploader.tsx

+84-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, {
55
PropsWithChildren,
66
useRef,
77
useEffect,
8+
useCallback,
89
} from 'react'
910
import classNames from 'classnames'
1011
import { Photograph, Failure } from '@nutui/icons-react'
@@ -70,6 +71,7 @@ export interface UploaderProps extends BasicComponent {
7071
beforeXhrUpload?: (xhr: XMLHttpRequest, options: any) => void
7172
beforeDelete?: (file: FileItem, files: FileItem[]) => boolean
7273
onFileItemClick?: (file: FileItem, index: number) => void
74+
enablePasteUpload?: boolean
7375
}
7476

7577
const defaultProps = {
@@ -99,6 +101,7 @@ const defaultProps = {
99101
beforeDelete: (file: FileItem, files: FileItem[]) => {
100102
return true
101103
},
104+
enablePasteUpload: false,
102105
} as UploaderProps
103106

104107
const InternalUploader: ForwardRefRenderFunction<
@@ -148,6 +151,7 @@ const InternalUploader: ForwardRefRenderFunction<
148151
beforeUpload,
149152
beforeXhrUpload,
150153
beforeDelete,
154+
enablePasteUpload,
151155
...restProps
152156
} = { ...defaultProps, ...props }
153157
const [fileList, setFileList] = usePropsValue({
@@ -307,7 +311,7 @@ const InternalUploader: ForwardRefRenderFunction<
307311
const reader = new FileReader()
308312
reader.onload = (event: ProgressEvent<FileReader>) => {
309313
fileItem.url = (event.target as FileReader).result as string
310-
// setFileList([...fileList, fileItem])
314+
setFileList([...fileList, fileItem])
311315
results.push(fileItem)
312316
}
313317
reader.readAsDataURL(file)
@@ -383,6 +387,85 @@ const InternalUploader: ForwardRefRenderFunction<
383387
onFileItemClick?.(file, index)
384388
}
385389

390+
const handlePaste = useCallback(
391+
(event: ClipboardEvent) => {
392+
if (!enablePasteUpload || disabled) return
393+
394+
const clipboardData = event.clipboardData
395+
if (!clipboardData) return
396+
397+
const files: File[] = []
398+
399+
if (clipboardData.items && clipboardData.items.length) {
400+
for (let i = 0; i < clipboardData.items.length; i++) {
401+
const item = clipboardData.items[i]
402+
if (item.kind === 'file' && item.type.startsWith('image/')) {
403+
const file = item.getAsFile()
404+
if (file) {
405+
files.push(file)
406+
}
407+
}
408+
}
409+
} else if (clipboardData.files && clipboardData.files.length) {
410+
for (let i = 0; i < clipboardData.files.length; i++) {
411+
const file = clipboardData.files[i]
412+
if (file.type.startsWith('image/')) {
413+
files.push(file)
414+
}
415+
}
416+
}
417+
418+
if (files.length) {
419+
if (beforeUpload) {
420+
beforeUpload(files).then((f: Array<File> | boolean) => {
421+
if (typeof f === 'boolean') return
422+
423+
const _files = filterFiles(new Array<File>().slice.call(f))
424+
if (_files.length) {
425+
readFile(_files)
426+
onChange?.([
427+
...fileList,
428+
...files.map((file) => ({
429+
name: file.name,
430+
type: file.type,
431+
status: 'ready',
432+
})),
433+
])
434+
}
435+
})
436+
} else {
437+
const _files = filterFiles(new Array<File>().slice.call(files))
438+
if (_files.length) {
439+
readFile(_files)
440+
onChange?.([
441+
...fileList,
442+
..._files.map((file) => ({
443+
name: file.name,
444+
type: file.type,
445+
status: 'ready',
446+
})),
447+
])
448+
}
449+
}
450+
}
451+
},
452+
[enablePasteUpload, disabled, beforeUpload, filterFiles, readFile, onChange]
453+
)
454+
455+
useEffect(() => {
456+
fileListRef.current = fileList
457+
458+
if (enablePasteUpload) {
459+
document.addEventListener('paste', handlePaste)
460+
}
461+
462+
return () => {
463+
if (enablePasteUpload) {
464+
document.removeEventListener('paste', handlePaste)
465+
}
466+
}
467+
}, [fileList, enablePasteUpload, handlePaste])
468+
386469
return (
387470
<div className={classes} {...restProps}>
388471
{(children || previewType === 'list') && (

0 commit comments

Comments
 (0)