mirror of
https://github.com/element-plus/element-plus.git
synced 2025-04-12 16:40:36 +08:00
feat(upload): upload component (#168)
* feat(upload): upload component * bump version * fix(upload): fix typo & doc \ Co-authored-by: Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com>
This commit is contained in:
parent
12814a868a
commit
cce1b5a252
@ -37,6 +37,7 @@ import ElDialog from '@element-plus/dialog'
|
||||
import ElCalendar from '@element-plus/calendar'
|
||||
import ElInfiniteScroll from '@element-plus/infinite-scroll'
|
||||
import ElDrawer from '@element-plus/drawer'
|
||||
import ElUpload from '@element-plus/upload'
|
||||
import ElTree from '@element-plus/tree'
|
||||
|
||||
export {
|
||||
@ -77,6 +78,7 @@ export {
|
||||
ElCalendar,
|
||||
ElInfiniteScroll,
|
||||
ElDrawer,
|
||||
ElUpload,
|
||||
ElTree,
|
||||
}
|
||||
|
||||
@ -119,6 +121,7 @@ const install = (app: App): void => {
|
||||
ElCalendar(app)
|
||||
ElInfiniteScroll(app)
|
||||
ElDrawer(app)
|
||||
ElUpload(app)
|
||||
ElTree(app)
|
||||
}
|
||||
|
||||
|
@ -3,3 +3,5 @@ import Progress from './src/index.vue'
|
||||
export default (app: App): void => {
|
||||
app.component(Progress.name, Progress)
|
||||
}
|
||||
|
||||
export const ElProgress = Progress
|
||||
|
8
packages/test-utils/make-mount.ts
Normal file
8
packages/test-utils/make-mount.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { merge } from 'lodash'
|
||||
|
||||
const makeMount = <C,O, E>(element: C, defaultOptions: O) => {
|
||||
return (props: (E | O) | (E & O)= {} as E) => mount(element, merge({}, defaultOptions, props))
|
||||
}
|
||||
|
||||
export default makeMount
|
68
packages/upload/__tests__/upload-dragger.spec.ts
Normal file
68
packages/upload/__tests__/upload-dragger.spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { provide, h, defineComponent } from 'vue'
|
||||
import makeMount from '../../test-utils/make-mount'
|
||||
import UploadDragger from '../src/upload-dragger.vue'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
|
||||
const Wrapper = defineComponent({
|
||||
props: {
|
||||
onDrop: Function,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
provide('uploader', { accept: 'video/*' })
|
||||
return () => h(UploadDragger, props, slots)
|
||||
},
|
||||
})
|
||||
const mount = makeMount(Wrapper, {
|
||||
slots: {
|
||||
default: () => AXIOM,
|
||||
},
|
||||
})
|
||||
|
||||
describe('<upload-dragger />', () => {
|
||||
describe('render test', () => {
|
||||
test('should render correct', () => {
|
||||
const wrapper = mount()
|
||||
|
||||
expect(wrapper.text()).toBe(AXIOM)
|
||||
})
|
||||
})
|
||||
|
||||
describe('functionality', () => {
|
||||
test('onDrag works', async () => {
|
||||
const wrapper = mount()
|
||||
await wrapper.find('.el-upload-dragger').trigger('dragover')
|
||||
expect(wrapper.classes('is-dragover')).toBe(true)
|
||||
})
|
||||
|
||||
test('ondrop works for any given video type', async () => {
|
||||
const onDrop = jest.fn()
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
onDrop,
|
||||
},
|
||||
})
|
||||
const dragger = wrapper.findComponent(UploadDragger)
|
||||
|
||||
await dragger.trigger('drop', {
|
||||
dataTransfer: {
|
||||
files: [{
|
||||
type: 'video/mp4',
|
||||
name: 'test.mp4',
|
||||
}],
|
||||
},
|
||||
})
|
||||
expect(onDrop).toHaveBeenCalledTimes(1)
|
||||
expect(dragger.emitted('file')).toHaveLength(1)
|
||||
await dragger.trigger('drop', {
|
||||
dataTransfer: {
|
||||
files: [{
|
||||
type: 'video/mov',
|
||||
name: 'test.mov',
|
||||
}],
|
||||
},
|
||||
})
|
||||
expect(dragger.emitted('file')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
80
packages/upload/__tests__/upload-list.spec.ts
Normal file
80
packages/upload/__tests__/upload-list.spec.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { h } from 'vue'
|
||||
import { EVENT_CODE } from '@element-plus/utils/aria'
|
||||
|
||||
import makeMount from '../../test-utils/make-mount'
|
||||
import UploadList from '../src/upload-list.vue'
|
||||
|
||||
const testName = 'test name'
|
||||
|
||||
const mount = makeMount(UploadList, {
|
||||
props: {
|
||||
files: [new File([], testName)],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
describe('<upload-list />', () => {
|
||||
|
||||
describe('render test', () => {
|
||||
test('should render correct', () => {
|
||||
const wrapper = mount({
|
||||
|
||||
slots: {
|
||||
default: ({ file }: { file: File; } ) => h('div', null, file.name),
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toBe(testName)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('functionalities', () => {
|
||||
|
||||
test('handle preview works', async () => {
|
||||
const preview = jest.fn()
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
handlePreview: preview,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('.el-upload-list__item-name').trigger('click')
|
||||
expect(preview).toHaveBeenCalled()
|
||||
|
||||
await wrapper.setProps({
|
||||
listType: 'picture-card',
|
||||
})
|
||||
|
||||
await wrapper.find('.el-upload-list__item-preview').trigger('click')
|
||||
expect(preview).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('handle delete works', async () => {
|
||||
const remove = jest.fn()
|
||||
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
onRemove: remove,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('.el-icon-close').trigger('click')
|
||||
expect(remove).toHaveBeenCalled()
|
||||
|
||||
await wrapper.find('.el-upload-list__item').trigger('keydown', {
|
||||
key: EVENT_CODE.delete,
|
||||
})
|
||||
|
||||
expect(remove).toHaveBeenCalledTimes(2)
|
||||
|
||||
await wrapper.setProps({
|
||||
listType: 'picture-card',
|
||||
})
|
||||
|
||||
await wrapper.find('.el-upload-list__item-delete').trigger('click')
|
||||
expect(remove).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
171
packages/upload/__tests__/upload.spec.ts
Normal file
171
packages/upload/__tests__/upload.spec.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { on } from '@element-plus/utils/dom'
|
||||
import { EVENT_CODE } from '../../utils/aria'
|
||||
|
||||
import makeMount from '../../test-utils/make-mount'
|
||||
import Upload from '../src/upload.vue'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
const action = 'test-action'
|
||||
|
||||
interface MockFile {
|
||||
name: string
|
||||
body: string
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
const mockGetFile = (element: HTMLInputElement, files: File[]) => {
|
||||
Object.defineProperty(element, 'files', {
|
||||
get() {
|
||||
return files
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const mount = makeMount(Upload, {
|
||||
props: {
|
||||
action,
|
||||
},
|
||||
slots: {
|
||||
default: () => AXIOM,
|
||||
},
|
||||
})
|
||||
|
||||
describe('<upload />', () => {
|
||||
describe('render test', () => {
|
||||
test('basic rendering', async () => {
|
||||
const wrapper = mount()
|
||||
expect(wrapper.text()).toEqual(AXIOM)
|
||||
await wrapper.setProps({
|
||||
drag: true,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.el-upload-dragger').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('functionality', () => {
|
||||
test('works with keydown & click', async () => {
|
||||
const wrapper = mount()
|
||||
|
||||
const click = jest.fn()
|
||||
on(wrapper.find('input').element, 'click', click)
|
||||
|
||||
await wrapper.trigger('click')
|
||||
expect(click).toHaveBeenCalled()
|
||||
await wrapper.trigger('keydown', {
|
||||
key: EVENT_CODE.enter,
|
||||
})
|
||||
expect(click).toHaveBeenCalledTimes(2)
|
||||
|
||||
await wrapper.trigger('keydown', {
|
||||
key: EVENT_CODE.space,
|
||||
})
|
||||
expect(click).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
test('works when upload file exceeds the limit', async () => {
|
||||
const onExceed = jest.fn()
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
onExceed,
|
||||
limit: 1,
|
||||
},
|
||||
})
|
||||
const fileList = [
|
||||
new File(['content'], 'test-file.txt'),
|
||||
new File(['content'], 'test-file.txt'),
|
||||
]
|
||||
mockGetFile(wrapper.find('input').element, fileList)
|
||||
|
||||
await wrapper.find('input').trigger('change')
|
||||
expect(onExceed).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('onStart works', async () => {
|
||||
const onStart = jest.fn()
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
onStart,
|
||||
autoUpload: false, // prevent auto upload
|
||||
},
|
||||
})
|
||||
const fileList = [new File(['content'], 'test-file.txt')]
|
||||
|
||||
mockGetFile(wrapper.find('input').element, fileList)
|
||||
await wrapper.find('input').trigger('change')
|
||||
|
||||
expect(onStart).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('beforeUpload works for rejecting upload', async () => {
|
||||
const beforeUpload = jest.fn(() => Promise.reject())
|
||||
const onRemove = jest.fn()
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
beforeUpload,
|
||||
onRemove,
|
||||
},
|
||||
})
|
||||
const fileList = [new File(['content'], 'test-file.txt')]
|
||||
|
||||
mockGetFile(wrapper.find('input').element, fileList)
|
||||
await wrapper.find('input').trigger('change')
|
||||
|
||||
expect(beforeUpload).toHaveBeenCalled()
|
||||
await nextTick()
|
||||
expect(onRemove).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('beforeUpload works for resolving upload', async () => {
|
||||
const beforeUpload = jest.fn(() => Promise.resolve())
|
||||
const httpRequest = jest.fn(() => Promise.resolve())
|
||||
const onSuccess = jest.fn()
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
beforeUpload,
|
||||
httpRequest,
|
||||
onSuccess,
|
||||
},
|
||||
})
|
||||
const fileList = [new File(['content'], 'test-file.txt')]
|
||||
|
||||
mockGetFile(wrapper.find('input').element, fileList)
|
||||
await wrapper.find('input').trigger('change')
|
||||
|
||||
expect(beforeUpload).toHaveBeenCalled()
|
||||
await nextTick()
|
||||
// await nextTick()
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
|
||||
const onError = jest.fn()
|
||||
await wrapper.setProps({
|
||||
httpRequest: jest.fn(() => Promise.reject()),
|
||||
onError,
|
||||
})
|
||||
await wrapper.find('input').trigger('change')
|
||||
await nextTick()
|
||||
expect(onError).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('onProgress should work', async () => {
|
||||
const onProgress = jest.fn()
|
||||
const httpRequest = jest.fn(({ onProgress }) => {
|
||||
onProgress()
|
||||
return Promise.resolve()
|
||||
})
|
||||
const wrapper = mount({
|
||||
props: {
|
||||
httpRequest,
|
||||
onProgress,
|
||||
},
|
||||
})
|
||||
const fileList = [new File(['content'], 'test-file.txt')]
|
||||
|
||||
mockGetFile(wrapper.find('input').element, fileList)
|
||||
await wrapper.find('input').trigger('change')
|
||||
await nextTick()
|
||||
expect(onProgress).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
42
packages/upload/doc/basic.vue
Normal file
42
packages/upload/doc/basic.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:before-remove="beforeRemove"
|
||||
:multiple="multiple"
|
||||
:limit="3"
|
||||
:on-exceed="handleExceed"
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-button size="small" type="primary">Click to upload</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: [{ name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' }, { name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' }],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRemove(file, fileList) {
|
||||
console.log(file, fileList)
|
||||
},
|
||||
handlePreview(file) {
|
||||
console.log(file)
|
||||
},
|
||||
handleExceed(files, fileList) {
|
||||
this.$message.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + fileList.length} totally`)
|
||||
},
|
||||
beforeRemove(file, fileList) {
|
||||
console.log(`Cancel the transfert of ${ file.name } ?`)
|
||||
console.log(fileList)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
17
packages/upload/doc/drop-to-upload.vue
Normal file
17
packages/upload/doc/drop-to-upload.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
drag
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:file-list="fileList"
|
||||
multiple
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">Drop file here or <em>click to upload</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
7
packages/upload/doc/index.stories.ts
Normal file
7
packages/upload/doc/index.stories.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
title: 'Upload',
|
||||
}
|
||||
|
||||
export { default as BasicUsage } from './basic.vue'
|
||||
export { default as ManualUpload } from './manual.vue'
|
||||
export { default as DropToUpload } from './drop-to-upload.vue'
|
32
packages/upload/doc/manual.vue
Normal file
32
packages/upload/doc/manual.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-demo"
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button size="small" type="primary">select file</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
style="margin-left: 10px;"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="submitUpload"
|
||||
>
|
||||
upload to server
|
||||
</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
submitUpload() {
|
||||
this.$refs.upload.submit()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
5
packages/upload/index.ts
Normal file
5
packages/upload/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { App } from 'vue'
|
||||
import Upload from './src/index.vue'
|
||||
export default (app: App): void => {
|
||||
app.component(Upload.name, Upload)
|
||||
}
|
12
packages/upload/package.json
Normal file
12
packages/upload/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/upload",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.3"
|
||||
}
|
||||
}
|
91
packages/upload/src/ajax.ts
Normal file
91
packages/upload/src/ajax.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import type {
|
||||
ElUploadProgressEvent,
|
||||
ElUploadRequestOptions,
|
||||
ElUploadAjaxError,
|
||||
} from './upload'
|
||||
|
||||
function getError(action: string, option: ElUploadRequestOptions, xhr: XMLHttpRequest) {
|
||||
let msg: string
|
||||
if (xhr.response) {
|
||||
msg = `${xhr.response.error || xhr.response}`
|
||||
} else if (xhr.responseText) {
|
||||
msg = `${xhr.responseText}`
|
||||
} else {
|
||||
msg = `fail to post ${action} ${xhr.status}`
|
||||
}
|
||||
|
||||
const err = new Error(msg) as ElUploadAjaxError
|
||||
err.status = xhr.status
|
||||
err.method = 'post'
|
||||
err.url = action
|
||||
return err
|
||||
}
|
||||
|
||||
function getBody(xhr: XMLHttpRequest): XMLHttpRequestResponseType {
|
||||
const text = xhr.responseText || xhr.response
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
export default function upload(option: ElUploadRequestOptions) {
|
||||
if (typeof XMLHttpRequest === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
const action = option.action
|
||||
|
||||
if (xhr.upload) {
|
||||
xhr.upload.onprogress = function progress(e) {
|
||||
if (e.total > 0) {
|
||||
(e as ElUploadProgressEvent).percent = e.loaded / e.total * 100
|
||||
}
|
||||
option.onProgress(e)
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
if (option.data) {
|
||||
Object.keys(option.data).forEach(key => {
|
||||
formData.append(key, option.data[key])
|
||||
})
|
||||
}
|
||||
|
||||
formData.append(option.filename, option.file, option.file.name)
|
||||
|
||||
xhr.onerror = function error() {
|
||||
option.onError(getError(action, option, xhr))
|
||||
}
|
||||
|
||||
xhr.onload = function onload() {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
return option.onError(getError(action, option, xhr))
|
||||
}
|
||||
|
||||
option.onSuccess(getBody(xhr))
|
||||
}
|
||||
|
||||
xhr.open('post', action, true)
|
||||
|
||||
if (option.withCredentials && 'withCredentials' in xhr) {
|
||||
xhr.withCredentials = true
|
||||
}
|
||||
|
||||
const headers = option.headers || {}
|
||||
|
||||
for (const item in headers) {
|
||||
if (headers.hasOwnProperty(item) && headers[item] !== null) {
|
||||
xhr.setRequestHeader(item, headers[item])
|
||||
}
|
||||
}
|
||||
xhr.send(formData)
|
||||
return xhr
|
||||
}
|
247
packages/upload/src/index.vue
Normal file
247
packages/upload/src/index.vue
Normal file
@ -0,0 +1,247 @@
|
||||
<script lang='ts'>
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
h,
|
||||
getCurrentInstance,
|
||||
inject,
|
||||
ref,
|
||||
provide,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
|
||||
import ajax from './ajax'
|
||||
import UploadList from './upload-list.vue'
|
||||
import Upload from './upload.vue'
|
||||
import useHandlers from './useHandlers'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type {
|
||||
ListType,
|
||||
UploadFile,
|
||||
FileHandler,
|
||||
FileResultHandler,
|
||||
} from './upload'
|
||||
|
||||
type PFileHandler<T> = PropType<FileHandler<T>>
|
||||
type PFileResultHandler<T = any> = PropType<FileResultHandler<T>>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElUpload',
|
||||
components: {
|
||||
Upload,
|
||||
UploadList,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object as PropType<Headers>,
|
||||
default: () => ({}),
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withCredentials: Boolean,
|
||||
showFileList: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'select',
|
||||
},
|
||||
beforeUpload: {
|
||||
type: Function as PFileHandler<void>,
|
||||
default: NOOP,
|
||||
},
|
||||
beforeRemove: {
|
||||
type: Function as PFileHandler<boolean>,
|
||||
default: NOOP,
|
||||
},
|
||||
onRemove: {
|
||||
type: Function as PFileHandler<void>,
|
||||
default: NOOP,
|
||||
},
|
||||
onChange: {
|
||||
type: Function as PFileHandler<void>,
|
||||
default: NOOP,
|
||||
},
|
||||
onPreview: {
|
||||
type: Function as PropType<() => void>,
|
||||
default: NOOP,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function as PFileResultHandler,
|
||||
default: NOOP,
|
||||
},
|
||||
onProgress: {
|
||||
type: Function as PFileResultHandler<ProgressEvent>,
|
||||
default: NOOP,
|
||||
},
|
||||
onError: {
|
||||
type: Function as PFileResultHandler<Error>,
|
||||
default: NOOP,
|
||||
},
|
||||
fileList: {
|
||||
type: Array as PropType<UploadFile[]>,
|
||||
default: () => {
|
||||
return [] as UploadFile[]
|
||||
},
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<ListType>,
|
||||
default: 'text' as ListType, // text,picture,picture-card
|
||||
},
|
||||
httpRequest: {
|
||||
type: Function,
|
||||
default: ajax,
|
||||
},
|
||||
disabled: Boolean,
|
||||
limit: {
|
||||
type: Number as PropType<Nullable<number>>,
|
||||
default: null,
|
||||
},
|
||||
onExceed: {
|
||||
type: Function,
|
||||
default: () => NOOP,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// init here
|
||||
const elForm = inject('elForm', {} as { disabled: boolean; })
|
||||
|
||||
const uploadDisabled = computed(() => {
|
||||
return props.disabled || elForm.disabled
|
||||
})
|
||||
|
||||
const {
|
||||
clearFiles,
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
handleRemove,
|
||||
submit,
|
||||
uploadRef,
|
||||
uploadFiles,
|
||||
} = useHandlers(props)
|
||||
|
||||
|
||||
provide('uploader', getCurrentInstance())
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
uploadFiles.value.forEach(file => {
|
||||
if (file.url && file.url.indexOf('blob:') === 0) {
|
||||
URL.revokeObjectURL(file.url)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
dragOver: ref(false),
|
||||
draging: ref(false),
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleRemove,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
uploadDisabled,
|
||||
uploadFiles,
|
||||
uploadRef,
|
||||
submit,
|
||||
clearFiles,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
let uploadList
|
||||
if (this.showFileList) {
|
||||
uploadList = h(
|
||||
UploadList,
|
||||
{
|
||||
disabled: this.uploadDisabled,
|
||||
listType: this.listType,
|
||||
files: this.uploadFiles,
|
||||
onRemove: this.handleRemove,
|
||||
handlePreview: this.onPreview,
|
||||
},
|
||||
{
|
||||
file: (props: { file: UploadFile; }) => {
|
||||
if (this.$slots.file) {
|
||||
return this.$slots.file({
|
||||
file: props.file,
|
||||
})
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
uploadList = null
|
||||
}
|
||||
|
||||
const uploadData = {
|
||||
type: this.type,
|
||||
drag: this.drag,
|
||||
action: this.action,
|
||||
multiple: this.multiple,
|
||||
'before-upload': this.beforeUpload,
|
||||
'with-credentials': this.withCredentials,
|
||||
headers: this.headers,
|
||||
name: this.name,
|
||||
data: this.data,
|
||||
accept: this.accept,
|
||||
fileList: this.uploadFiles,
|
||||
autoUpload: this.autoUpload,
|
||||
listType: this.listType,
|
||||
disabled: this.uploadDisabled,
|
||||
limit: this.limit,
|
||||
'on-exceed': this.onExceed,
|
||||
'on-start': this.handleStart,
|
||||
'on-progress': this.handleProgress,
|
||||
'on-success': this.handleSuccess,
|
||||
'on-error': this.handleError,
|
||||
'on-preview': this.onPreview,
|
||||
'on-remove': this.handleRemove,
|
||||
'http-request': this.httpRequest,
|
||||
ref: 'uploadRef',
|
||||
}
|
||||
const trigger = this.$slots.trigger || this.$slots.default
|
||||
const uploadComponent = h(Upload, uploadData, {
|
||||
default: () => trigger?.(),
|
||||
})
|
||||
return h('div', [
|
||||
this.listType === 'picture-card' ? uploadList : null,
|
||||
this.$slots.trigger
|
||||
? [uploadComponent, this.$slots.default()]
|
||||
: uploadComponent,
|
||||
this.$slots.tip?.(),
|
||||
this.listType !== 'picture-card' ? uploadList : null,
|
||||
])
|
||||
},
|
||||
})
|
||||
</script>
|
78
packages/upload/src/upload-dragger.vue
Normal file
78
packages/upload/src/upload-dragger.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'el-upload-dragger': true,
|
||||
'is-dragover': dragover
|
||||
}"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="onDragover"
|
||||
@dragleave.prevent="dragover = false"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, inject } from 'vue'
|
||||
|
||||
import type { ElUpload } from './upload'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElUploadDrag',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['file'],
|
||||
setup(props, { emit }) {
|
||||
const uploader = inject('uploader', {} as ElUpload)
|
||||
const dragover = ref(false)
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
if (props.disabled || !uploader) return
|
||||
const accept = uploader.accept
|
||||
dragover.value = false
|
||||
if (!accept) {
|
||||
emit('file', e.dataTransfer.files)
|
||||
return
|
||||
}
|
||||
emit(
|
||||
'file',
|
||||
Array.from(e.dataTransfer.files).filter(file => {
|
||||
const { type, name } = file
|
||||
const extension =
|
||||
name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : ''
|
||||
const baseType = type.replace(/\/.*$/, '')
|
||||
return accept
|
||||
.split(',')
|
||||
.map(type => type.trim())
|
||||
.filter(type => type)
|
||||
.some(acceptedType => {
|
||||
if (acceptedType.startsWith('.')) {
|
||||
return extension === acceptedType
|
||||
}
|
||||
if (/\/\*$/.test(acceptedType)) {
|
||||
return baseType === acceptedType.replace(/\/\*$/, '')
|
||||
}
|
||||
if (/^[^\/]+\/[^\/]+$/.test(acceptedType)) {
|
||||
return type === acceptedType
|
||||
}
|
||||
return false
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function onDragover() {
|
||||
if (!props.disabled) dragover.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
dragover,
|
||||
onDrop,
|
||||
onDragover,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
127
packages/upload/src/upload-list.vue
Normal file
127
packages/upload/src/upload-list.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<transition-group
|
||||
tag="ul"
|
||||
:class="[
|
||||
'el-upload-list',
|
||||
'el-upload-list--' + listType,
|
||||
{ 'is-disabled': disabled }
|
||||
]"
|
||||
name="el-list"
|
||||
>
|
||||
<li
|
||||
v-for="(file, idx) in files"
|
||||
:key="idx"
|
||||
:class="['el-upload-list__item', 'is-' + file.status, focusing ? 'focusing' : '']"
|
||||
tabindex="0"
|
||||
@keydown.delete="!disabled && handleRemove($event, file)"
|
||||
@focus="focusing = true"
|
||||
@blur="focusing = false"
|
||||
@click="onFileClicked"
|
||||
>
|
||||
<slot :file="file">
|
||||
<img
|
||||
v-if="file.status !== 'uploading' && ['picture-card', 'picture'].includes(listType)"
|
||||
class="el-upload-list__item-thumbnail"
|
||||
:src="file.url"
|
||||
alt=""
|
||||
>
|
||||
<a class="el-upload-list__item-name" @click="handleClick(file)">
|
||||
<i class="el-icon-document"></i>{{ file.name }}
|
||||
</a>
|
||||
<label class="el-upload-list__item-status-label">
|
||||
<i
|
||||
:class="{
|
||||
'el-icon-upload-success': true,
|
||||
'el-icon-circle-check': listType === 'text',
|
||||
'el-icon-check': ['picture-card', 'picture'].includes(listType)
|
||||
}"
|
||||
></i>
|
||||
</label>
|
||||
<i v-if="!disabled" class="el-icon-close" @click="handleRemove($event, file)"></i>
|
||||
<!-- Due to close btn only appears when li gets focused disappears after li gets blurred, thus keyboard navigation can never reach close btn-->
|
||||
<!-- This is a bug which needs to be fixed -->
|
||||
<!-- TODO: Fix the incorrect navigation interaction -->
|
||||
<i v-if="!disabled" class="el-icon-close-tip">{{ t('el.upload.deleteTip') }}</i>
|
||||
<el-progress
|
||||
v-if="file.status === 'uploading'"
|
||||
:type="listType === 'picture-card' ? 'circle' : 'line'"
|
||||
:stroke-width="listType === 'picture-card' ? 6 : 2"
|
||||
:percentage="parsePercentage(file.percentage)"
|
||||
/>
|
||||
<span v-if="listType === 'picture-card'" class="el-upload-list__item-actions">
|
||||
<span
|
||||
class="el-upload-list__item-preview"
|
||||
@click="handlePreview(file)"
|
||||
>
|
||||
<i class="el-icon-zoom-in"></i>
|
||||
</span>
|
||||
<span
|
||||
v-if="!disabled"
|
||||
class="el-upload-list__item-delete"
|
||||
@click="handleRemove($event, file)"
|
||||
>
|
||||
<i class="el-icon-delete"></i>
|
||||
</span>
|
||||
</span>
|
||||
</slot>
|
||||
</li>
|
||||
</transition-group>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
|
||||
import { t } from '@element-plus/locale'
|
||||
import { ElProgress } from '@element-plus/progress'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElUploadList',
|
||||
components: { ElProgress },
|
||||
props: {
|
||||
files: {
|
||||
type: Array as PropType<File[]>,
|
||||
default: () => [] as File[],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
handlePreview: {
|
||||
type: Function as PropType<(file: File) => void>,
|
||||
default: () => NOOP,
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||
default: 'text',
|
||||
},
|
||||
},
|
||||
emits: ['remove'],
|
||||
setup(props, { emit }) {
|
||||
const parsePercentage = (val: string) => {
|
||||
return parseInt(val, 10)
|
||||
}
|
||||
|
||||
const handleClick = (file: File) => {
|
||||
props.handlePreview(file)
|
||||
}
|
||||
|
||||
const onFileClicked = (e: Event) => {
|
||||
(e.target as HTMLElement).focus()
|
||||
}
|
||||
|
||||
const handleRemove = (e: Event, file: File) => {
|
||||
emit('remove', file)
|
||||
}
|
||||
return {
|
||||
focusing: ref(false),
|
||||
parsePercentage,
|
||||
handleClick,
|
||||
handleRemove,
|
||||
onFileClicked,
|
||||
t,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
80
packages/upload/src/upload.d.ts
vendored
Normal file
80
packages/upload/src/upload.d.ts
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
export type ListType = 'text' | 'picture' | 'picture-card'
|
||||
|
||||
export type UploadStatus = 'ready' | 'uploading' | 'success' | 'fail'
|
||||
|
||||
export type UploadFile = {
|
||||
name: string
|
||||
percentage?: number
|
||||
status: UploadStatus
|
||||
size: number
|
||||
response?: unknown
|
||||
uid: number
|
||||
url?: string
|
||||
raw: ElFile
|
||||
}
|
||||
|
||||
export interface ElFile extends File {
|
||||
uid: number
|
||||
}
|
||||
|
||||
export interface ElUploadProgressEvent extends ProgressEvent {
|
||||
percent: number
|
||||
}
|
||||
|
||||
export interface ElUploadAjaxError extends Error {
|
||||
status: number
|
||||
method: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface ElUploadRequestOptions {
|
||||
action: string
|
||||
data: Record<string, string | Blob>
|
||||
filename: string
|
||||
file: File
|
||||
headers: Headers
|
||||
onError: (e: Error) => void
|
||||
onProgress: (e: ProgressEvent) => void
|
||||
onSuccess: (response: XMLHttpRequestResponseType) => unknown
|
||||
withCredentials: boolean
|
||||
}
|
||||
|
||||
export type FileHandler<T = void> = (file: UploadFile, uploadFiles: UploadFile[]) => T
|
||||
export type FileResultHandler<T = any> = (param: T, file: UploadFile, uploadFiles: UploadFile[]) => void
|
||||
|
||||
export interface IUseHandlersProps {
|
||||
listType: ListType
|
||||
fileList: UploadFile[]
|
||||
beforeUpload?: FileHandler
|
||||
beforeRemove?: FileHandler<Promise<any> | boolean>
|
||||
onRemove?: FileHandler
|
||||
onChange?: FileHandler
|
||||
onPreview?: () => void
|
||||
onSuccess?: FileResultHandler
|
||||
onProgress?: FileResultHandler<ProgressEvent>
|
||||
onError?: FileResultHandler<Error>
|
||||
}
|
||||
|
||||
export interface ElUpload extends IUseHandlersProps {
|
||||
accept: string
|
||||
headers?: Headers
|
||||
data?: Record<string, unknown>
|
||||
multiple?: boolean
|
||||
name?: string
|
||||
drag?: boolean
|
||||
withCredentials?: boolean
|
||||
showFileList?: boolean
|
||||
type?: string
|
||||
dragOver: boolean
|
||||
genUid: () => number
|
||||
tempIndex: number
|
||||
handleError: () => void
|
||||
handleProgress: () => void
|
||||
handleRemove: () => void
|
||||
handleStart: () => void
|
||||
handleSuccess: () => void
|
||||
uploadDisabled: boolean
|
||||
uploadFiles: UploadFile[]
|
||||
submit: () => void
|
||||
clearFiles: () => void
|
||||
}
|
282
packages/upload/src/upload.vue
Normal file
282
packages/upload/src/upload.vue
Normal file
@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['el-upload', `el-upload--${listType}`]"
|
||||
tabindex="0"
|
||||
@click="handleClick"
|
||||
@keydown.self.enter.space="handleKeydown"
|
||||
>
|
||||
<template v-if="drag">
|
||||
<upload-dragger :disabled="disabled" @file="uploadFiles">
|
||||
<slot></slot>
|
||||
</upload-dragger>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<input
|
||||
ref="inputRef"
|
||||
class="el-upload__input"
|
||||
type="file"
|
||||
:name="name"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
@change="handleChange"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
|
||||
import ajax from './ajax'
|
||||
import UploadDragger from './upload-dragger.vue'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { ListType, UploadFile, ElFile } from './upload'
|
||||
|
||||
type IFileHanlder = (
|
||||
file: Nullable<ElFile[]>,
|
||||
fileList?: UploadFile[],
|
||||
) => unknown
|
||||
|
||||
type AjaxEventListener = (e: ProgressEvent, file: ElFile) => unknown
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UploadDragger,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => null,
|
||||
},
|
||||
headers: {
|
||||
type: Object as PropType<Nullable<Partial<Headers>>>,
|
||||
default: () => null,
|
||||
},
|
||||
withCredentials: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean as PropType<Nullable<boolean>>,
|
||||
default: null,
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
onStart: {
|
||||
type: Function as PropType<(file: File) => void>,
|
||||
default: NOOP as (file: File) => void,
|
||||
},
|
||||
onProgress: {
|
||||
type: Function as PropType<AjaxEventListener>,
|
||||
default: NOOP as AjaxEventListener,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function as PropType<AjaxEventListener>,
|
||||
default: NOOP as AjaxEventListener,
|
||||
},
|
||||
onError: {
|
||||
type: Function as PropType<AjaxEventListener>,
|
||||
default: NOOP as AjaxEventListener,
|
||||
},
|
||||
beforeUpload: {
|
||||
type: Function as PropType<
|
||||
(file: File) => Promise<File | Blob> | boolean | unknown
|
||||
>,
|
||||
default: NOOP as (file: File) => void,
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onPreview: {
|
||||
type: Function as PropType<IFileHanlder>,
|
||||
default: NOOP as IFileHanlder,
|
||||
},
|
||||
onRemove: {
|
||||
type: Function as PropType<
|
||||
(file: Nullable<FileList>, rawFile: ElFile) => void
|
||||
>,
|
||||
default: NOOP as (file: Nullable<FileList>, rawFile: ElFile) => void,
|
||||
},
|
||||
fileList: {
|
||||
type: Array as PropType<UploadFile[]>,
|
||||
default: () => [] as UploadFile[],
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<ListType>,
|
||||
default: 'text',
|
||||
},
|
||||
httpRequest: {
|
||||
type: Function as
|
||||
| PropType<typeof ajax>
|
||||
| PropType<(...args: unknown[]) => Promise<unknown>>,
|
||||
default: () => ajax,
|
||||
},
|
||||
disabled: Boolean,
|
||||
limit: {
|
||||
type: Number as PropType<Nullable<number>>,
|
||||
default: null,
|
||||
},
|
||||
onExceed: {
|
||||
type: Function as PropType<
|
||||
(files: FileList, fileList: UploadFile[]) => void
|
||||
>,
|
||||
default: NOOP,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const reqs = ref({} as Indexable<XMLHttpRequest | Promise<any>>)
|
||||
const mouseover = ref(false)
|
||||
const inputRef = ref(null as Nullable<HTMLInputElement>)
|
||||
|
||||
function uploadFiles(files: FileList) {
|
||||
if (props.limit && props.fileList.length + files.length > props.limit) {
|
||||
props.onExceed(files, props.fileList)
|
||||
return
|
||||
}
|
||||
let postFiles = Array.from(files)
|
||||
if (!props.multiple) {
|
||||
postFiles = postFiles.slice(0, 1)
|
||||
}
|
||||
if (postFiles.length === 0) {
|
||||
return
|
||||
}
|
||||
postFiles.forEach(rawFile => {
|
||||
props.onStart(rawFile)
|
||||
if (props.autoUpload) upload(rawFile as ElFile)
|
||||
})
|
||||
}
|
||||
|
||||
function upload(rawFile: ElFile) {
|
||||
inputRef.value.value = null
|
||||
if (!props.beforeUpload) {
|
||||
return post(rawFile)
|
||||
}
|
||||
const before = props.beforeUpload(rawFile)
|
||||
if (before instanceof Promise) {
|
||||
before
|
||||
.then(processedFile => {
|
||||
const fileType = Object.prototype.toString.call(processedFile)
|
||||
if (fileType === '[object File]' || fileType === '[object Blob]') {
|
||||
if (fileType === '[object Blob]') {
|
||||
processedFile = new File([processedFile], rawFile.name, {
|
||||
type: rawFile.type,
|
||||
})
|
||||
}
|
||||
for (const p in rawFile) {
|
||||
if (rawFile.hasOwnProperty(p)) {
|
||||
processedFile[p] = rawFile[p]
|
||||
}
|
||||
}
|
||||
post(processedFile)
|
||||
} else {
|
||||
post(rawFile)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
props.onRemove(null, rawFile)
|
||||
})
|
||||
} else if (before !== false) {
|
||||
post(rawFile)
|
||||
} else {
|
||||
props.onRemove(null, rawFile)
|
||||
}
|
||||
}
|
||||
function abort(file) {
|
||||
const _reqs = reqs.value
|
||||
if (file) {
|
||||
let uid = file
|
||||
if (file.uid) uid = file.uid
|
||||
if (_reqs[uid]) {
|
||||
(_reqs[uid] as XMLHttpRequest).abort()
|
||||
}
|
||||
} else {
|
||||
Object.keys(_reqs).forEach(uid => {
|
||||
if (_reqs[uid]) (_reqs[uid] as XMLHttpRequest).abort()
|
||||
delete _reqs[uid]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function post(rawFile: ElFile) {
|
||||
const { uid } = rawFile
|
||||
const options = {
|
||||
headers: props.headers,
|
||||
withCredentials: props.withCredentials,
|
||||
file: rawFile,
|
||||
data: props.data,
|
||||
filename: props.name,
|
||||
action: props.action,
|
||||
onProgress: e => {
|
||||
props.onProgress(e, rawFile)
|
||||
},
|
||||
onSuccess: res => {
|
||||
props.onSuccess(res, rawFile)
|
||||
delete reqs.value[uid]
|
||||
},
|
||||
onError: err => {
|
||||
props.onError(err, rawFile)
|
||||
delete reqs.value[uid]
|
||||
},
|
||||
}
|
||||
const req = props.httpRequest(options)
|
||||
reqs.value[uid] = req
|
||||
if (req instanceof Promise) {
|
||||
req.then(options.onSuccess, options.onError)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(e: DragEvent) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
uploadFiles(files)
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
inputRef.value.value = null
|
||||
inputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown() {
|
||||
handleClick()
|
||||
}
|
||||
|
||||
return {
|
||||
reqs,
|
||||
mouseover,
|
||||
inputRef,
|
||||
abort,
|
||||
post,
|
||||
handleChange,
|
||||
handleClick,
|
||||
handleKeydown,
|
||||
upload,
|
||||
uploadFiles,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
152
packages/upload/src/useHandlers.ts
Normal file
152
packages/upload/src/useHandlers.ts
Normal file
@ -0,0 +1,152 @@
|
||||
|
||||
import { ref, watch } from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
|
||||
// Inline types
|
||||
import type { ListType, UploadFile, ElFile, ElUploadProgressEvent, IUseHandlersProps } from './upload'
|
||||
type UploadRef = {
|
||||
abort: (file: UploadFile) => void
|
||||
upload: (file: ElFile) => void
|
||||
}
|
||||
// helpers
|
||||
function getFile(rawFile: ElFile, uploadFiles: UploadFile[]) {
|
||||
return uploadFiles.find(file => file.uid === rawFile.uid)
|
||||
}
|
||||
|
||||
function genUid(seed: number) {
|
||||
return Date.now() + seed
|
||||
}
|
||||
|
||||
export default (props: IUseHandlersProps) => {
|
||||
|
||||
const uploadFiles = ref<UploadFile[]>([])
|
||||
const uploadRef = ref<UploadRef>(null)
|
||||
|
||||
let tempIndex = 1
|
||||
|
||||
function abort(file: UploadFile) {
|
||||
uploadRef.value.abort(file)
|
||||
}
|
||||
|
||||
function clearFiles() {
|
||||
uploadFiles.value = []
|
||||
}
|
||||
|
||||
function handleError(err: Error, rawFile: ElFile) {
|
||||
const file = getFile(rawFile, uploadFiles.value)
|
||||
file.status = 'fail'
|
||||
uploadFiles.value.splice(uploadFiles.value.indexOf(file), 1)
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
|
||||
function handleProgress(ev: ElUploadProgressEvent, rawFile: ElFile) {
|
||||
const file = getFile(rawFile, uploadFiles.value)
|
||||
props.onProgress(ev, file, uploadFiles.value)
|
||||
file.status = 'uploading'
|
||||
file.percentage = ev.percent || 0
|
||||
}
|
||||
|
||||
function handleSuccess(res: any, rawFile: ElFile) {
|
||||
const file = getFile(rawFile, uploadFiles.value)
|
||||
if (file) {
|
||||
file.status = 'success'
|
||||
file.response = res
|
||||
props.onSuccess(res, file, uploadFiles.value)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleStart(rawFile: ElFile) {
|
||||
const uid = genUid(tempIndex++)
|
||||
rawFile.uid = uid
|
||||
const file: UploadFile = {
|
||||
name: rawFile.name,
|
||||
percentage: 0,
|
||||
status: 'ready',
|
||||
size: rawFile.size,
|
||||
raw: rawFile,
|
||||
uid,
|
||||
}
|
||||
if (props.listType === 'picture-card' || props.listType === 'picture') {
|
||||
try {
|
||||
file.url = URL.createObjectURL(rawFile)
|
||||
} catch (err) {
|
||||
console.error('[Element Error][Upload]', err)
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
uploadFiles.value.push(file)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
|
||||
function handleRemove(file: UploadFile, raw: ElFile) {
|
||||
if (raw) {
|
||||
file = getFile(raw, uploadFiles.value)
|
||||
}
|
||||
const doRemove = () => {
|
||||
abort(file)
|
||||
const fileList = uploadFiles.value
|
||||
fileList.splice(fileList.indexOf(file), 1)
|
||||
props.onRemove(file, fileList)
|
||||
}
|
||||
if (!props.beforeRemove) {
|
||||
doRemove()
|
||||
} else if (typeof props.beforeRemove === 'function') {
|
||||
const before = props.beforeRemove(file, uploadFiles.value)
|
||||
if (before instanceof Promise) {
|
||||
before.then(() => {
|
||||
doRemove()
|
||||
}).catch(NOOP)
|
||||
} else if (before !== false) {
|
||||
doRemove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
uploadFiles.value
|
||||
.filter(file => file.status === 'ready')
|
||||
.forEach(file => {
|
||||
uploadRef.value.upload(file.raw)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.listType, (val: ListType) => {
|
||||
if (val === 'picture-card' || val === 'picture') {
|
||||
uploadFiles.value = uploadFiles.value.map(file => {
|
||||
if (!file.url && file.raw) {
|
||||
try {
|
||||
file.url = URL.createObjectURL(file.raw)
|
||||
} catch (err) {
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.fileList, (fileList: UploadFile[]) => {
|
||||
uploadFiles.value = fileList.map(file => {
|
||||
file.uid = file.uid || genUid(tempIndex++)
|
||||
file.status = file.status || 'success'
|
||||
return file
|
||||
})
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
return {
|
||||
clearFiles,
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
handleRemove,
|
||||
submit,
|
||||
uploadFiles,
|
||||
uploadRef,
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ Upload files by clicking or drag-and-drop
|
||||
### Click to upload files
|
||||
|
||||
:::demo Customize upload button type and text using `slot`. Set `limit` and `on-exceed` to limit the maximum number of uploads allowed and specify method when the limit is exceeded. Plus, you can abort removing a file in the `before-remove` hook.
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
@ -15,34 +16,55 @@ Upload files by clicking or drag-and-drop
|
||||
multiple
|
||||
:limit="3"
|
||||
:on-exceed="handleExceed"
|
||||
:file-list="fileList">
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-button size="small" type="primary">Click to upload</el-button>
|
||||
<div slot="tip" class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: [{name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}, {name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}]
|
||||
};
|
||||
fileList: [
|
||||
{
|
||||
name: 'food.jpeg',
|
||||
url:
|
||||
'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
|
||||
},
|
||||
{
|
||||
name: 'food2.jpeg',
|
||||
url:
|
||||
'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRemove(file, fileList) {
|
||||
console.log(file, fileList);
|
||||
console.log(file, fileList)
|
||||
},
|
||||
handlePreview(file) {
|
||||
console.log(file);
|
||||
console.log(file)
|
||||
},
|
||||
handleExceed(files, fileList) {
|
||||
this.$message.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + fileList.length} totally`);
|
||||
this.$message.warning(
|
||||
`The limit is 3, you selected ${
|
||||
files.length
|
||||
} files this time, add up to ${
|
||||
files.length + fileList.length
|
||||
} totally`,
|
||||
)
|
||||
},
|
||||
beforeRemove(file, fileList) {
|
||||
return this.$confirm(`Cancel the transfert of ${ file.name } ?`);
|
||||
}
|
||||
}
|
||||
return this.$confirm(`Cancel the transfert of ${file.name} ?`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### User avatar upload
|
||||
@ -50,14 +72,16 @@ Upload files by clicking or drag-and-drop
|
||||
Use `before-upload` hook to limit the upload file format and size.
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:before-upload="beforeAvatarUpload">
|
||||
<img v-if="imageUrl" :src="imageUrl" class="avatar">
|
||||
:before-upload="beforeAvatarUpload"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
|
||||
</el-upload>
|
||||
|
||||
@ -70,7 +94,7 @@ Use `before-upload` hook to limit the upload file format and size.
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatar-uploader .el-upload:hover {
|
||||
border-color: #409EFF;
|
||||
border-color: #409eff;
|
||||
}
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
@ -91,29 +115,30 @@ Use `before-upload` hook to limit the upload file format and size.
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
imageUrl: ''
|
||||
};
|
||||
imageUrl: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAvatarSuccess(res, file) {
|
||||
this.imageUrl = URL.createObjectURL(file.raw);
|
||||
this.imageUrl = URL.createObjectURL(file.raw)
|
||||
},
|
||||
beforeAvatarUpload(file) {
|
||||
const isJPG = file.type === 'image/jpeg';
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
const isJPG = file.type === 'image/jpeg'
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isJPG) {
|
||||
this.$message.error('Avatar picture must be JPG format!');
|
||||
this.$message.error('Avatar picture must be JPG format!')
|
||||
}
|
||||
if (!isLt2M) {
|
||||
this.$message.error('Avatar picture size can not exceed 2MB!');
|
||||
this.$message.error('Avatar picture size can not exceed 2MB!')
|
||||
}
|
||||
return isJPG && isLt2M;
|
||||
}
|
||||
}
|
||||
return isJPG && isLt2M
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Photo Wall
|
||||
@ -121,37 +146,40 @@ Use `before-upload` hook to limit the upload file format and size.
|
||||
Use `list-type` to change the fileList style.
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
list-type="picture-card"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:on-remove="handleRemove">
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<i class="el-icon-plus"></i>
|
||||
</el-upload>
|
||||
<el-dialog :visible.sync="dialogVisible">
|
||||
<img width="100%" :src="dialogImageUrl" alt="">
|
||||
<img width="100%" :src="dialogImageUrl" alt="" />
|
||||
</el-dialog>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dialogImageUrl: '',
|
||||
dialogVisible: false
|
||||
};
|
||||
dialogVisible: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRemove(file, fileList) {
|
||||
console.log(file, fileList);
|
||||
console.log(file, fileList)
|
||||
},
|
||||
handlePictureCardPreview(file) {
|
||||
this.dialogImageUrl = file.url;
|
||||
this.dialogVisible = true;
|
||||
}
|
||||
}
|
||||
this.dialogImageUrl = file.url
|
||||
this.dialogVisible = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Custom file thumbnail
|
||||
@ -159,17 +187,15 @@ Use `list-type` to change the fileList style.
|
||||
Use `scoped-slot` to change default thumbnail template.
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
action="#"
|
||||
list-type="picture-card"
|
||||
:auto-upload="false">
|
||||
<i slot="default" class="el-icon-plus"></i>
|
||||
<div slot="file" slot-scope="{file}">
|
||||
<img
|
||||
class="el-upload-list__item-thumbnail"
|
||||
:src="file.url" alt=""
|
||||
>
|
||||
<el-upload action="#" list-type="picture-card" :auto-upload="false">
|
||||
<template #default>
|
||||
<i class="el-icon-plus"></i>
|
||||
</template>
|
||||
<template #file="{file}">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span
|
||||
class="el-upload-list__item-preview"
|
||||
@ -193,9 +219,10 @@ Use `scoped-slot` to change default thumbnail template.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog :visible.sync="dialogVisible">
|
||||
<img width="100%" :src="dialogImageUrl" alt="">
|
||||
<img width="100%" :src="dialogImageUrl" alt="" />
|
||||
</el-dialog>
|
||||
<script>
|
||||
export default {
|
||||
@ -203,29 +230,31 @@ Use `scoped-slot` to change default thumbnail template.
|
||||
return {
|
||||
dialogImageUrl: '',
|
||||
dialogVisible: false,
|
||||
disabled: false
|
||||
};
|
||||
disabled: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRemove(file) {
|
||||
console.log(file);
|
||||
console.log(file)
|
||||
},
|
||||
handlePictureCardPreview(file) {
|
||||
this.dialogImageUrl = file.url;
|
||||
this.dialogVisible = true;
|
||||
this.dialogImageUrl = file.url
|
||||
this.dialogVisible = true
|
||||
},
|
||||
handleDownload(file) {
|
||||
console.log(file);
|
||||
}
|
||||
}
|
||||
console.log(file)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### FileList with thumbnail
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
@ -233,28 +262,45 @@ Use `scoped-slot` to change default thumbnail template.
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:file-list="fileList"
|
||||
list-type="picture">
|
||||
list-type="picture"
|
||||
>
|
||||
<el-button size="small" type="primary">Click to upload</el-button>
|
||||
<div slot="tip" class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
jpg/png files with a size less than 500kb
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: [{name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}, {name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}]
|
||||
};
|
||||
fileList: [
|
||||
{
|
||||
name: 'food.jpeg',
|
||||
url:
|
||||
'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
|
||||
},
|
||||
{
|
||||
name: 'food2.jpeg',
|
||||
url:
|
||||
'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRemove(file, fileList) {
|
||||
console.log(file, fileList);
|
||||
console.log(file, fileList)
|
||||
},
|
||||
handlePreview(file) {
|
||||
console.log(file);
|
||||
}
|
||||
}
|
||||
console.log(file)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### File list control
|
||||
@ -262,36 +308,48 @@ Use `scoped-slot` to change default thumbnail template.
|
||||
Use `on-change` hook function to control upload file list
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:on-change="handleChange"
|
||||
:file-list="fileList">
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-button size="small" type="primary">Click to upload</el-button>
|
||||
<div slot="tip" class="el-upload__tip">jpg/png files with a size less than 500kb</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
jpg/png files with a size less than 500kb
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: [{
|
||||
name: 'food.jpeg',
|
||||
url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'
|
||||
}, {
|
||||
name: 'food2.jpeg',
|
||||
url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'
|
||||
}]
|
||||
};
|
||||
fileList: [
|
||||
{
|
||||
name: 'food.jpeg',
|
||||
url:
|
||||
'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
|
||||
},
|
||||
{
|
||||
name: 'food2.jpeg',
|
||||
url:
|
||||
'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(file, fileList) {
|
||||
this.fileList = fileList.slice(-3);
|
||||
}
|
||||
}
|
||||
this.fileList = fileList.slice(-3)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Drag to upload
|
||||
@ -299,6 +357,7 @@ Use `on-change` hook function to control upload file list
|
||||
You can drag your file to a certain area to upload it.
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
@ -307,37 +366,58 @@ You can drag your file to a certain area to upload it.
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:file-list="fileList"
|
||||
multiple>
|
||||
multiple
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">Drop file here or <em>click to upload</em></div>
|
||||
<div class="el-upload__tip" slot="tip">jpg/png files with a size less than 500kb</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
jpg/png files with a size less than 500kb
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Manual upload
|
||||
|
||||
:::demo
|
||||
|
||||
```html
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
ref="upload"
|
||||
action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:auto-upload="false">
|
||||
<el-button slot="trigger" size="small" type="primary">select file</el-button>
|
||||
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">upload to server</el-button>
|
||||
<div class="el-upload__tip" slot="tip">jpg/png files with a size less than 500kb</div>
|
||||
:auto-upload="false"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button size="small" type="primary">select file</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
style="margin-left: 10px;"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="submitUpload"
|
||||
>upload to server</el-button
|
||||
>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
jpg/png files with a size less than 500kb
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
submitUpload() {
|
||||
this.$refs.upload.submit();
|
||||
}
|
||||
}
|
||||
this.$refs.upload.submit()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Attributes
|
||||
|
Loading…
x
Reference in New Issue
Block a user