refactor(components): refactor upload (#6014)

* refactor(components): refactor ElUpload

* refactor(components): refactor upload

* test: use jsx

* refactor: resolve review comments

* fix: ts error

* refactor: re-order imports

* refactor: rename

* fix: infinity watch

* refactor: rename

* refactor: address PR comments

Co-authored-by: Kevin <sxzz@sxzz.moe>
This commit is contained in:
C.Y.Kun 2022-03-05 23:09:31 +08:00 committed by GitHub
parent ce10babc22
commit 13ffea1114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 986 additions and 1031 deletions

View File

@ -25,5 +25,6 @@
"i18n-ally.localesPaths": "packages/locale/lang",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledFrameworks": ["vue", "vue-sfc"],
"i18n-ally.keystyle": "nested"
"i18n-ally.keystyle": "nested",
"iconify.includes": ["ep"]
}

View File

@ -91,33 +91,33 @@ upload/manual
## Attributes
| Attribute | Description | Type | Accepted Values | Default |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------- | ------- |
| action | required, request URL | string | — | — |
| headers | request headers | object | — | — |
| method | set upload request method | string | post/put/patch | post |
| multiple | whether uploading multiple files is permitted | boolean | — | — |
| data | additions options of request | object | — | — |
| name | key name for uploaded file | string | — | file |
| with-credentials | whether cookies are sent | boolean | — | false |
| show-file-list | whether to show the uploaded file list | boolean | — | true |
| drag | whether to activate drag and drop mode | boolean | — | false |
| accept | accepted [file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept), will not work when `thumbnail-mode` is `true` | string | — | — |
| on-preview | hook function when clicking the uploaded files | function(file) | — | — |
| on-remove | hook function when files are removed | function(file, fileList) | — | — |
| on-success | hook function when uploaded successfully | function(response, file, fileList) | — | — |
| on-error | hook function when some errors occurs | function(err, file, fileList) | — | — |
| on-progress | hook function when some progress occurs | function(event, file, fileList) | — | — |
| on-change | hook function when select file or upload file success or upload file fail | function(file, fileList) | — | — |
| before-upload | hook function before uploading with the file to be uploaded as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, uploading will be aborted | function(file) | — | — |
| before-remove | hook function before removing a file with the file and file list as its parameters. If `false` is returned or a `Promise` is returned and then is rejected, removing will be aborted. | function(file, fileList) | — | — |
| file-list | default uploaded files, e.g. [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] | array | — | [] |
| list-type | type of fileList | string | text/picture/picture-card | text |
| auto-upload | whether to auto upload file | boolean | — | true |
| http-request | override default xhr behavior, allowing you to implement your own upload-file's request | function | — | — |
| disabled | whether to disable upload | boolean | — | false |
| limit | maximum number of uploads allowed | number | — | — |
| on-exceed | hook function when limit is exceeded | function(files, fileList) | — | - |
| Attribute | Description | Type | Accepted Values | Default |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------- | ------- |
| action | required, request URL | string | — | — |
| headers | request headers | object | — | — |
| method | set upload request method | string | post/put/patch | post |
| multiple | whether uploading multiple files is permitted | boolean | — | — |
| data | additions options of request | object | — | — |
| name | key name for uploaded file | string | — | file |
| with-credentials | whether cookies are sent | boolean | — | false |
| show-file-list | whether to show the uploaded file list | boolean | — | true |
| drag | whether to activate drag and drop mode | boolean | — | false |
| accept | accepted [file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept), will not work when `thumbnail-mode` is `true` | string | — | — |
| on-preview | hook function when clicking the uploaded files | function(file: UploadFile): void | — | — |
| on-remove | hook function when files are removed | function(file: UploadFile, fileList: UploadFile[]): void | — | — |
| on-success | hook function when uploaded successfully | function(response: any, file: UploadFile, fileList: UploadFile[]): void | — | — |
| on-error | hook function when some errors occurs | function(error, file: UploadFile, fileList: UploadFile[]): void | — | — |
| on-progress | hook function when some progress occurs | function(evt: UploadProgressEvent, file: UploadFile, fileList: UploadFile[]): void | — | — |
| on-change | hook function when select file or upload file success or upload file fail | function(file: UploadFile, fileList: UploadFile[]): void | — | — |
| before-upload | hook function before uploading with the file to be uploaded as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, uploading will be aborted | function(file: RawFile): Awaitable<void \| undefined \| null \| boolean \| File \| Blob> | — | — |
| before-remove | hook function before removing a file with the file and file list as its parameters. If `false` is returned or a `Promise` is returned and then is rejected, removing will be aborted. | function(file: UploadFile, fileList: UploadFile[]): Awaitable\<boolean\> | — | — |
| file-list | default uploaded files, e.g. [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] | Array\<UploadFile\> | — | [] |
| list-type | type of fileList | string | text/picture/picture-card | text |
| auto-upload | whether to auto upload file | boolean | — | true |
| http-request | override default xhr behavior, allowing you to implement your own upload-file's request | (options: UploadRequestOptions) => XMLHttpRequest \| Promise\<unknown\> | — | — |
| disabled | whether to disable upload | boolean | — | false |
| limit | maximum number of uploads allowed | number | — | — |
| on-exceed | hook function when limit is exceeded | function(files: File[], uploadFiles: UploadFile[]): void | — | - |
## Slots
@ -130,10 +130,10 @@ upload/manual
## Methods
| Methods Name | Description | Parameters | Default |
| ------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- |
| clearFiles | clear the file list (this method is not supported in the `before-upload` hook). | UploadStatus[] (UploadStatus = 'ready' \| 'uploading' \| 'success' \| 'fail') | ['ready', 'uploading', 'success', 'fail'] |
| abort | cancel upload request | ( file: fileList's item ) | - |
| submit | upload the file list manually | — | - |
| handleStart | select the file manually | ( file: files' item) | - |
| handleRemove | remove the file manually | ( file: fileList's item ) | - |
| Methods Name | Description | Parameters | Default |
| ------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------- |
| clearFiles | clear the file list (this method is not supported in the `before-upload` hook). | (status: Array\<"ready"\|"uploading"\|"success"\|"fail"\>) => void | ['ready', 'uploading', 'success', 'fail'] |
| abort | cancel upload request | (file: UploadFile) => void | - |
| submit | upload the file list manually | — | - |
| handleStart | select the file manually | (file: File) => void | - |
| handleRemove | remove the file manually. `file` and `rawFile` has been merged. `rawFile` will be removed in `v2.2.0` | (file: UploadFile \| UploadRawFile, rawFile?:UploadRawFile) => void | - |

View File

@ -1,7 +1,7 @@
<template>
<el-upload action="#" list-type="picture-card" :auto-upload="false">
<template #default>
<el-icon><plus /></el-icon>
<el-icon><Plus /></el-icon>
</template>
<template #file="{ file }">
<div>
@ -18,14 +18,14 @@
class="el-upload-list__item-delete"
@click="handleDownload(file)"
>
<el-icon><download /></el-icon>
<el-icon><Download /></el-icon>
</span>
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<el-icon><delete /></el-icon>
<el-icon><Delete /></el-icon>
</span>
</span>
</div>

View File

@ -5,7 +5,7 @@
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><plus /></el-icon>
<el-icon><Plus /></el-icon>
</el-upload>
<el-dialog v-model="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt="" />

View File

@ -1,5 +1,6 @@
import { provide, h, defineComponent } from 'vue'
import { provide, h, defineComponent, computed } from 'vue'
import makeMount from '@element-plus/test-utils/make-mount'
import { uploadContextKey } from '@element-plus/tokens'
import UploadDragger from '../src/upload-dragger.vue'
const AXIOM = 'Rem is the best girl'
@ -9,7 +10,7 @@ const Wrapper = defineComponent({
onDrop: Function,
},
setup(props, { slots }) {
provide('uploader', { accept: 'video/*' })
provide(uploadContextKey, { accept: computed(() => 'video/*') })
return () => h(UploadDragger, props, slots)
},
})

View File

@ -1,4 +1,3 @@
import { h } from 'vue'
import { EVENT_CODE } from '@element-plus/constants'
import makeMount from '@element-plus/test-utils/make-mount'
@ -17,7 +16,7 @@ describe('<upload-list />', () => {
test('should render correct', () => {
const wrapper = mount({
slots: {
default: ({ file }: { file: File }) => h('div', null, file.name),
default: ({ file }: { file: File }) => <div>{file.name}</div>,
},
})
expect(wrapper.text()).toBe(testName)

View File

@ -1,9 +1,10 @@
import { nextTick } from 'vue'
import { computed, defineComponent, h, nextTick, provide } from 'vue'
import makeMount from '@element-plus/test-utils/make-mount'
import { on } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import Upload from '../src/upload.vue'
import { uploadContextKey } from '@element-plus/tokens'
import UploadContent from '../src/upload-content.vue'
const AXIOM = 'Rem is the best girl'
const action = 'test-action'
@ -16,7 +17,20 @@ const mockGetFile = (element: HTMLInputElement, files: File[]) => {
})
}
const mount = makeMount(Upload, {
const Wrapper = defineComponent({
props: {
action: {
type: String,
required: true,
},
},
setup(props, { slots }) {
provide(uploadContextKey, { accept: computed(() => 'video/*') })
return () => h(UploadContent, props, slots)
},
})
const mount = makeMount(Wrapper, {
props: {
action,
},

View File

@ -1,13 +1,10 @@
import Upload from './src/index.vue'
import { withInstall } from '@element-plus/utils'
import Upload from './src/upload.vue'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils'
export const ElUpload = withInstall(Upload)
export default ElUpload
Upload.install = (app: App): void => {
app.component(Upload.name, Upload)
}
const _Upload = Upload as SFCWithInstall<typeof Upload>
export default _Upload
export const ElUpload = _Upload
export * from './src/upload'
export * from './src/upload-content'
export * from './src/upload-list'
export * from './src/upload-dragger'

View File

@ -1,13 +1,29 @@
import { hasOwn } from '@element-plus/utils'
import { isNil } from 'lodash-unified'
import { throwError } from '@element-plus/utils'
import type {
ElUploadProgressEvent,
ElUploadRequestOptions,
ElUploadAjaxError,
} from './upload.type'
UploadRequestHandler,
UploadProgressEvent,
UploadRequestOptions,
} from './upload'
const SCOPE = 'ElUpload'
export class UploadAjaxError extends Error {
status: number
method: string
url: string
constructor(message: string, status: number, method: string, url: string) {
super(message)
this.status = status
this.method = method
this.url = url
}
}
function getError(
action: string,
option: ElUploadRequestOptions,
option: UploadRequestOptions,
xhr: XMLHttpRequest
) {
let msg: string
@ -19,11 +35,7 @@ function getError(
msg = `fail to ${option.method} ${action} ${xhr.status}`
}
const err = new Error(msg) as ElUploadAjaxError
err.status = xhr.status
err.method = option.method
err.url = action
return err
return new UploadAjaxError(msg, xhr.status, option.method, action)
}
function getBody(xhr: XMLHttpRequest): XMLHttpRequestResponseType {
@ -39,44 +51,40 @@ function getBody(xhr: XMLHttpRequest): XMLHttpRequestResponseType {
}
}
export default function upload(option: ElUploadRequestOptions) {
if (typeof XMLHttpRequest === 'undefined') {
return
}
export const ajaxUpload: UploadRequestHandler = (option) => {
if (typeof XMLHttpRequest === 'undefined')
throwError(SCOPE, 'XMLHttpRequest is undefined')
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])
xhr.upload.addEventListener('progress', (evt) => {
const progressEvt = evt as UploadProgressEvent
progressEvt.percent = evt.total > 0 ? (evt.loaded / evt.total) * 100 : 0
option.onProgress(progressEvt)
})
}
const formData = new FormData()
if (option.data) {
for (const [key, value] of Object.entries(option.data)) {
if (Array.isArray(value)) formData.append(key, ...value)
else formData.append(key, value)
}
}
formData.append(option.filename, option.file, option.file.name)
xhr.onerror = function error() {
xhr.addEventListener('error', () => {
option.onError(getError(action, option, xhr))
}
})
xhr.onload = function onload() {
xhr.addEventListener('load', () => {
if (xhr.status < 200 || xhr.status >= 300) {
return option.onError(getError(action, option, xhr))
}
option.onSuccess(getBody(xhr))
}
})
xhr.open(option.method, action, true)
@ -85,17 +93,13 @@ export default function upload(option: ElUploadRequestOptions) {
}
const headers = option.headers || {}
for (const item in headers) {
if (hasOwn(headers, item) && headers[item] !== null) {
xhr.setRequestHeader(item, headers[item])
}
}
if (headers instanceof Headers) {
headers.forEach((value, key) => {
xhr.setRequestHeader(key, value)
})
headers.forEach((value, key) => xhr.setRequestHeader(key, value))
} else {
for (const [key, value] of Object.entries(headers)) {
if (isNil(value)) continue
xhr.setRequestHeader(key, String(value))
}
}
xhr.send(formData)

View File

@ -1,254 +0,0 @@
<script lang="ts">
import {
computed,
defineComponent,
h,
getCurrentInstance,
inject,
ref,
provide,
onBeforeUnmount,
} from 'vue'
import { NOOP } from '@vue/shared'
import { elFormKey } from '@element-plus/tokens'
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 { ElFormContext } from '@element-plus/tokens'
import type { Nullable } from '@element-plus/utils'
import type {
ListType,
UploadFile,
FileHandler,
FileResultHandler,
} from './upload.type'
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: () => ({}),
},
method: {
type: String,
default: 'post',
},
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) {
const elForm = inject(elFormKey, {} as ElFormContext)
const uploadDisabled = computed(() => {
return props.disabled || elForm.disabled
})
const {
abort,
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 {
abort,
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,
},
this.$slots.file
? {
default: (props: { file: UploadFile }) => {
return this.$slots.file({
file: props.file,
})
},
}
: 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,
method: this.method,
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>

View File

@ -0,0 +1,57 @@
import { NOOP } from '@vue/shared'
import { buildProps, definePropType } from '@element-plus/utils'
import { uploadBaseProps } from './upload'
import type { ExtractPropTypes } from 'vue'
import type {
UploadRawFile,
UploadFile,
UploadProgressEvent,
UploadHooks,
} from './upload'
import type UploadContent from './upload-content.vue'
import type { UploadAjaxError } from './ajax'
export const uploadContentProps = buildProps({
...uploadBaseProps,
beforeUpload: {
type: definePropType<UploadHooks['beforeUpload']>(Function),
default: NOOP,
},
onRemove: {
type: definePropType<
(file: UploadFile | UploadRawFile, rawFile?: UploadRawFile) => void
>(Function),
default: NOOP,
},
onStart: {
type: definePropType<(rawFile: UploadRawFile) => void>(Function),
default: NOOP,
},
onSuccess: {
type: definePropType<(response: any, rawFile: UploadRawFile) => unknown>(
Function
),
default: NOOP,
},
onProgress: {
type: definePropType<
(evt: UploadProgressEvent, rawFile: UploadRawFile) => void
>(Function),
default: NOOP,
},
onError: {
type: definePropType<
(err: UploadAjaxError, rawFile: UploadRawFile) => void
>(Function),
default: NOOP,
},
onExceed: {
type: definePropType<UploadHooks['onExceed']>(Function),
default: NOOP,
},
} as const)
export type UploadContentProps = ExtractPropTypes<typeof uploadContentProps>
export type UploadContentInstance = InstanceType<typeof UploadContent>

View File

@ -0,0 +1,186 @@
<template>
<div
:class="[ns.b(), ns.m(listType)]"
tabindex="0"
@click="handleClick"
@keydown.self.enter.space="handleKeydown"
>
<template v-if="drag">
<upload-dragger :disabled="disabled" @file="uploadFiles">
<slot />
</upload-dragger>
</template>
<template v-else>
<slot />
</template>
<input
ref="inputRef"
:class="ns.e('input')"
:name="name"
:multiple="multiple"
:accept="accept"
type="file"
@change="handleChange"
/>
</div>
</template>
<script lang="ts" setup>
import { shallowRef } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { entriesOf } from '@element-plus/utils'
import UploadDragger from './upload-dragger.vue'
import { uploadContentProps } from './upload-content'
import { genFileId } from './upload'
import type {
UploadRequestOptions,
UploadRawFile,
UploadFile,
UploadHooks,
} from './upload'
defineOptions({
name: 'ElUploadContent',
})
const props = defineProps(uploadContentProps)
const ns = useNamespace('upload')
const requests = shallowRef<Record<string, XMLHttpRequest | Promise<unknown>>>(
{}
)
const inputRef = shallowRef<HTMLInputElement>()
const uploadFiles = (files: File[]) => {
if (files.length === 0) return
const { autoUpload, limit, fileList, multiple, onStart, onExceed } = props
if (limit && fileList.length + files.length > limit) {
onExceed(files, fileList)
return
}
if (!multiple) {
files = files.slice(0, 1)
}
for (const file of files) {
const rawFile = file as UploadRawFile
rawFile.uid = genFileId()
onStart(rawFile)
if (autoUpload) upload(rawFile)
}
}
const upload = async (rawFile: UploadRawFile) => {
inputRef.value!.value = ''
if (!props.beforeUpload) {
return doUpload(rawFile)
}
let hookResult: Exclude<ReturnType<UploadHooks['beforeUpload']>, Promise<any>>
try {
hookResult = await props.beforeUpload(rawFile)
} catch {
hookResult = false
}
if (hookResult === false) {
props.onRemove(rawFile)
return
}
let file: File = rawFile
if (hookResult instanceof Blob) {
if (hookResult instanceof File) {
file = hookResult
} else {
file = new File([hookResult], rawFile.name, {
type: rawFile.type,
})
}
for (const key of Object.keys(rawFile)) {
file[key] = rawFile[key]
}
}
doUpload(rawFile)
}
const doUpload = (rawFile: UploadRawFile) => {
const {
headers,
data,
method,
withCredentials,
name: filename,
action,
onProgress,
onSuccess,
onError,
httpRequest,
} = props
const { uid } = rawFile
const options: UploadRequestOptions = {
headers: headers || {},
withCredentials,
file: rawFile,
data,
method,
filename,
action,
onProgress: (evt) => {
onProgress(evt, rawFile)
},
onSuccess: (res) => {
onSuccess(res, rawFile)
delete requests.value[uid]
},
onError: (err) => {
onError(err, rawFile)
delete requests.value[uid]
},
}
const request = httpRequest(options)
requests.value[uid] = request
if (request instanceof Promise) {
request.then(options.onSuccess, options.onError)
}
}
const handleChange = (e: Event) => {
const files = (e.target as HTMLInputElement).files
if (!files) return
uploadFiles(Array.from(files))
}
const handleClick = () => {
if (!props.disabled) {
inputRef.value!.value = ''
inputRef.value!.click()
}
}
const handleKeydown = () => {
handleClick()
}
const abort = (file?: UploadFile) => {
const _reqs = entriesOf(requests.value).filter(
file ? ([uid]) => String(file.uid) === uid : () => true
)
_reqs.forEach(([uid, req]) => {
if (req instanceof XMLHttpRequest) req.abort()
delete requests.value[uid]
})
}
defineExpose({
abort,
upload,
})
</script>

View File

@ -0,0 +1,19 @@
import { buildProps, isArray } from '@element-plus/utils'
import type { ExtractPropTypes } from 'vue'
import type UploadDragger from './upload-dragger.vue'
export const uploadDraggerProps = buildProps({
disabled: {
type: Boolean,
default: false,
},
} as const)
export type UploadDraggerProps = ExtractPropTypes<typeof uploadDraggerProps>
export const uploadDraggerEmits = {
file: (file: File[]) => isArray(file),
}
export type UploadDraggerEmits = typeof uploadDraggerEmits
export type UploadDraggerInstance = InstanceType<typeof UploadDragger>

View File

@ -8,71 +8,71 @@
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, inject } from 'vue'
<script lang="ts" setup>
import { ref, inject } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import type { ElUpload } from './upload.type'
import { uploadContextKey } from '@element-plus/tokens'
import { throwError } from '@element-plus/utils/error'
import { uploadDraggerEmits, uploadDraggerProps } from './upload-dragger'
export default defineComponent({
const COMPONENT_NAME = 'ElUploadDrag'
defineOptions({
name: 'ElUploadDrag',
props: {
disabled: {
type: Boolean,
default: false,
},
},
emits: ['file'],
setup(props, { emit }) {
const uploader = inject('uploader', {} as ElUpload)
const ns = useNamespace('upload')
const dragover = ref(false)
function onDrop(e: DragEvent) {
if (props.disabled || !uploader) return
const accept = uploader.props?.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 {
ns,
dragover,
onDrop,
onDragover,
}
},
})
const props = defineProps(uploadDraggerProps)
const emit = defineEmits(uploadDraggerEmits)
const uploaderContext = inject(uploadContextKey)
if (!uploaderContext) {
throwError(
COMPONENT_NAME,
'usage: <el-upload><el-upload-dragger /></el-upload>'
)
}
const ns = useNamespace('upload')
const dragover = ref(false)
const onDrop = (e: DragEvent) => {
if (props.disabled) return
dragover.value = false
const files = Array.from(e.dataTransfer!.files)
const accept = uploaderContext.accept.value
if (!accept) {
emit('file', files)
return
}
const filesFiltered = files.filter((file) => {
const { type, name } = file
const extension = name.includes('.') ? `.${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
})
})
emit('file', filesFiltered)
}
const onDragover = () => {
if (!props.disabled) dragover.value = true
}
</script>

View File

@ -0,0 +1,33 @@
import { NOOP } from '@vue/shared'
import { buildProps, definePropType, mutable } from '@element-plus/utils'
import { uploadListTypes } from './upload'
import type { ExtractPropTypes } from 'vue'
import type { UploadFile, UploadHooks, UploadFiles } from './upload'
import type UploadList from './upload-list.vue'
export const uploadListProps = buildProps({
files: {
type: definePropType<UploadFiles>(Array),
default: () => mutable([]),
},
disabled: {
type: Boolean,
default: false,
},
handlePreview: {
type: definePropType<UploadHooks['onPreview']>(Function),
default: NOOP,
},
listType: {
type: String,
values: uploadListTypes,
default: 'text',
},
} as const)
export type UploadListProps = ExtractPropTypes<typeof uploadListProps>
export const uploadListEmits = {
remove: (file: UploadFile) => !!file,
}
export type UploadListEmits = typeof uploadListEmits
export type UploadListInstance = InstanceType<typeof UploadList>

View File

@ -10,7 +10,7 @@
>
<li
v-for="file in files"
:key="file.uid || file"
:key="file.uid || file.name"
:class="[
nsUpload.be('list', 'item'),
nsUpload.is(file.status),
@ -33,7 +33,7 @@
alt=""
/>
<a :class="nsUpload.be('list', 'item-name')" @click="handleClick(file)">
<el-icon :class="nsIcon.m('document')"><document /></el-icon>
<el-icon :class="nsIcon.m('document')"><Document /></el-icon>
{{ file.name }}
</a>
<label :class="nsUpload.be('list', 'item-status-label')">
@ -47,7 +47,7 @@
v-else-if="['picture-card', 'picture'].includes(listType)"
:class="[nsIcon.m('upload-success'), nsIcon.m('check')]"
>
<check />
<Check />
</el-icon>
</label>
<el-icon
@ -55,7 +55,7 @@
:class="nsIcon.m('close')"
@click="handleRemove(file)"
>
<close />
<Close />
</el-icon>
<!-- 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 -->
@ -67,7 +67,7 @@
v-if="file.status === 'uploading'"
:type="listType === 'picture-card' ? 'circle' : 'line'"
:stroke-width="listType === 'picture-card' ? 6 : 2"
:percentage="+file.percentage"
:percentage="Number(file.percentage)"
style="margin-top: 0.5rem"
/>
<span
@ -85,16 +85,15 @@
:class="nsUpload.be('list', 'item-delete')"
@click="handleRemove(file)"
>
<el-icon :class="nsIcon.m('delete')"><delete /></el-icon>
<el-icon :class="nsIcon.m('delete')"><Delete /></el-icon>
</span>
</span>
</slot>
</li>
</transition-group>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { NOOP } from '@vue/shared'
<script lang="ts" setup>
import { ref } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import {
Document,
@ -107,67 +106,32 @@ import {
import { useLocale, useNamespace } from '@element-plus/hooks'
import ElProgress from '@element-plus/components/progress'
import type { PropType } from 'vue'
import type { UploadFile } from './upload.type'
import { uploadListEmits, uploadListProps } from './upload-list'
import type { UploadFile } from './upload'
export default defineComponent({
defineOptions({
name: 'ElUploadList',
components: {
ElProgress,
ElIcon,
Document,
Delete,
Close,
ZoomIn,
Check,
CircleCheck,
},
props: {
files: {
type: Array as PropType<UploadFile[]>,
default: () => [] as File[],
},
disabled: {
type: Boolean,
default: false,
},
handlePreview: {
type: Function as PropType<(file: UploadFile) => void>,
default: () => NOOP,
},
listType: {
type: String as PropType<'picture' | 'picture-card' | 'text'>,
default: 'text',
},
},
emits: ['remove'],
setup(props, { emit }) {
const { t } = useLocale()
const nsUpload = useNamespace('upload')
const nsIcon = useNamespace('icon')
const nsList = useNamespace('list')
const handleClick = (file: UploadFile) => {
props.handlePreview(file)
}
const onFileClicked = (e: Event) => {
;(e.target as HTMLElement).focus()
}
const handleRemove = (file: UploadFile) => {
emit('remove', file)
}
return {
focusing: ref(false),
handleClick,
handleRemove,
onFileClicked,
t,
nsUpload,
nsIcon,
nsList,
}
},
})
const props = defineProps(uploadListProps)
const emit = defineEmits(uploadListEmits)
const { t } = useLocale()
const nsUpload = useNamespace('upload')
const nsIcon = useNamespace('icon')
const nsList = useNamespace('list')
const focusing = ref(false)
const handleClick = (file: UploadFile) => {
props.handlePreview(file)
}
const onFileClicked = (e: Event) => {
;(e.target as HTMLElement).focus()
}
const handleRemove = (file: UploadFile) => {
emit('remove', file)
}
</script>

View File

@ -0,0 +1,181 @@
import { NOOP } from '@vue/shared'
import { buildProps, definePropType, mutable } from '@element-plus/utils'
import { ajaxUpload } from './ajax'
import type { UploadAjaxError } from './ajax'
import type { Awaitable } from '@element-plus/utils'
import type { ExtractPropTypes } from 'vue'
import type Upload from './upload.vue'
export const uploadListTypes = ['text', 'picture', 'picture-card'] as const
let fileId = 1
export const genFileId = () => Date.now() + fileId++
export type UploadStatus = 'ready' | 'uploading' | 'success' | 'fail'
export interface UploadProgressEvent extends ProgressEvent {
percent: number
}
export interface UploadRequestOptions {
action: string
method: string
data: Record<string, string | Blob | [string | Blob, string]>
filename: string
file: File
headers: Headers | Record<string, string | number | null | undefined>
onError: (evt: UploadAjaxError) => void
onProgress: (evt: UploadProgressEvent) => void
onSuccess: (response: any) => void
withCredentials: boolean
}
export interface UploadFile {
name: string
percentage?: number
status: UploadStatus
size: number
response?: unknown
uid: number
url?: string
raw: UploadRawFile
}
export type UploadFiles = UploadFile[]
export interface UploadRawFile extends File {
uid: number
}
export type UploadRequestHandler = (
options: UploadRequestOptions
) => XMLHttpRequest | Promise<unknown>
export interface UploadHooks {
beforeUpload: (
rawFile: UploadRawFile
) => Awaitable<void | undefined | null | boolean | File | Blob>
beforeRemove: (
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => Awaitable<boolean>
onRemove: (uploadFile: UploadFile, uploadFiles: UploadFiles) => void
onChange: (uploadFile: UploadFile, uploadFiles: UploadFiles) => void
onPreview: (uploadFile: UploadFile) => void
onSuccess: (
response: any,
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => void
onProgress: (
evt: UploadProgressEvent,
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => void
onError: (
error: Error,
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => void
onExceed: (files: File[], uploadFiles: UploadFiles) => void
}
export const uploadBaseProps = buildProps({
action: {
type: String,
required: true,
},
headers: {
type: definePropType<Headers | Record<string, any>>(Object),
},
method: {
type: String,
default: 'post',
},
data: {
type: Object,
default: () => mutable({} as const),
},
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',
},
fileList: {
type: definePropType<UploadFiles>(Array),
default: () => mutable([] as const),
},
autoUpload: {
type: Boolean,
default: true,
},
listType: {
type: String,
values: uploadListTypes,
default: 'text',
},
httpRequest: {
type: definePropType<UploadRequestHandler>(Function),
default: ajaxUpload,
},
disabled: Boolean,
limit: Number,
} as const)
export const uploadProps = buildProps({
...uploadBaseProps,
beforeUpload: {
type: definePropType<UploadHooks['beforeUpload']>(Function),
default: NOOP,
},
beforeRemove: {
type: definePropType<UploadHooks['beforeRemove']>(Function),
},
onRemove: {
type: definePropType<UploadHooks['onRemove']>(Function),
default: NOOP,
},
onChange: {
type: definePropType<UploadHooks['onChange']>(Function),
default: NOOP,
},
onPreview: {
type: definePropType<UploadHooks['onPreview']>(Function),
default: NOOP,
},
onSuccess: {
type: definePropType<UploadHooks['onSuccess']>(Function),
default: NOOP,
},
onProgress: {
type: definePropType<UploadHooks['onProgress']>(Function),
default: NOOP,
},
onError: {
type: definePropType<UploadHooks['onError']>(Function),
default: NOOP,
},
onExceed: {
type: definePropType<UploadHooks['onExceed']>(Function),
default: NOOP,
},
} as const)
export type UploadProps = ExtractPropTypes<typeof uploadProps>
export type UploadInstance = InstanceType<typeof Upload>

View File

@ -1,88 +0,0 @@
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
method: 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
}

View File

@ -1,292 +1,117 @@
<template>
<div
:class="[ns.b(), ns.m(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="ns.e('input')"
type="file"
:name="name"
<div>
<upload-list
v-if="isPictureCard && showFileList"
:disabled="disabled"
:list-type="listType"
:files="uploadFiles"
:handle-preview="onPreview"
@remove="handleRemove"
>
<template v-if="$slots.file" #default="{ file }">
<slot name="file" :file="file"></slot>
</template>
</upload-list>
<upload-content
ref="uploadRef"
:type="type"
:drag="drag"
:action="action"
:multiple="multiple"
:with-credentials="withCredentials"
:headers="headers"
:method="method"
:name="name"
:data="data"
:accept="accept"
@change="handleChange"
/>
:file-list="uploadFiles"
:auto-upload="autoUpload"
:list-type="listType"
:disabled="disabled"
:limit="limit"
:http-request="httpRequest"
:before-upload="beforeUpload"
:on-exceed="onExceed"
:on-start="handleStart"
:on-progress="handleProgress"
:on-success="handleSuccess"
:on-error="handleError"
:on-remove="handleRemove"
>
<template #default>
<slot v-if="$slots.trigger" name="trigger" />
<slot v-if="!$slots.trigger && $slots.default" />
</template>
</upload-content>
<slot v-if="$slots.trigger" />
<slot name="tip" />
<upload-list
v-if="!isPictureCard && showFileList"
:disabled="disabled"
:list-type="listType"
:files="uploadFiles"
:handle-preview="onPreview"
@remove="handleRemove"
>
<template v-if="$slots.file" #default="{ file }">
<slot name="file" :file="file" />
</template>
</upload-list>
</div>
</template>
<script lang="ts" setup>
import { computed, provide, onBeforeUnmount, toRef, shallowRef } from 'vue'
import { uploadContextKey } from '@element-plus/tokens'
import { useDisabled } from '@element-plus/hooks'
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { NOOP } from '@vue/shared'
import { hasOwn } from '@element-plus/utils'
import UploadList from './upload-list.vue'
import UploadContent from './upload-content.vue'
import { useHandlers } from './use-handlers'
import { uploadProps } from './upload'
import type { UploadContentInstance } from './upload-content'
import { useNamespace } from '@element-plus/hooks'
import ajax from './ajax'
import UploadDragger from './upload-dragger.vue'
defineOptions({
name: 'ElUpload',
})
import type { PropType } from 'vue'
import type { Nullable } from '@element-plus/utils'
import type { ListType, UploadFile, ElFile } from './upload.type'
const props = defineProps(uploadProps)
const disabled = useDisabled()
type IFileHanlder = (
file: Nullable<ElFile[]>,
fileList?: UploadFile[]
) => unknown
const uploadRef = shallowRef<UploadContentInstance>()
const {
abort,
submit,
clearFiles,
uploadFiles,
handleStart,
handleError,
handleRemove,
handleSuccess,
handleProgress,
} = useHandlers(props, uploadRef)
type AjaxEventListener = (e: ProgressEvent, file: ElFile) => unknown
const isPictureCard = computed(() => props.listType === 'picture-card')
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,
},
method: {
type: String,
default: 'post',
},
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 Record<string, XMLHttpRequest | Promise<any>>)
const ns = useNamespace('upload')
const mouseover = ref(false)
const inputRef = ref(null as Nullable<HTMLInputElement>)
onBeforeUnmount(() => {
uploadFiles.value.forEach(({ url }) => {
if (url?.startsWith('blob:')) URL.revokeObjectURL(url)
})
})
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)
})
}
provide(uploadContextKey, {
accept: toRef(props, 'accept'),
})
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 (hasOwn(rawFile, 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,
method: props.method,
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 {
ns,
reqs,
mouseover,
inputRef,
abort,
post,
handleChange,
handleClick,
handleKeydown,
upload,
uploadFiles,
}
},
defineExpose({
/** @description cancel upload request */
abort,
/** @description upload the file list manually */
submit,
/** @description clear the file list */
clearFiles,
/** @description select the file manually */
handleStart,
/** @description remove the file manually */
handleRemove,
})
</script>

View File

@ -0,0 +1,187 @@
import { ref, watch } from 'vue'
import { debugWarn, throwError } from '@element-plus/utils'
import { useDeprecated } from '@element-plus/hooks'
import { genFileId } from './upload'
import type { ShallowRef } from 'vue'
import type {
UploadContentProps,
UploadContentInstance,
} from './upload-content'
import type {
UploadRawFile,
UploadFile,
UploadProps,
UploadStatus,
UploadFiles,
} from './upload'
const SCOPE = 'ElUpload'
const revokeObjectURL = (file: UploadFile) => {
if (file.url?.startsWith('blob:')) {
URL.revokeObjectURL(file.url)
}
}
export const useHandlers = (
props: UploadProps,
uploadRef: ShallowRef<UploadContentInstance | undefined>
) => {
const uploadFiles = ref<UploadFiles>([])
const getFile = (rawFile: UploadRawFile) =>
uploadFiles.value.find((file) => file.uid === rawFile.uid)
function abort(file: UploadFile) {
uploadRef.value?.abort(file)
}
function clearFiles(
/** @default ['ready', 'uploading', 'success', 'fail'] */
states: UploadStatus[] = ['ready', 'uploading', 'success', 'fail']
) {
uploadFiles.value = uploadFiles.value.filter(
(row) => !states.includes(row.status)
)
}
const handleError: UploadContentProps['onError'] = (err, rawFile) => {
const file = getFile(rawFile)
if (!file) return
file.status = 'fail'
uploadFiles.value.splice(uploadFiles.value.indexOf(file), 1)
props.onError(err, file, uploadFiles.value)
props.onChange(file, uploadFiles.value)
}
const handleProgress: UploadContentProps['onProgress'] = (evt, rawFile) => {
const file = getFile(rawFile)
if (!file) return
props.onProgress(evt, file, uploadFiles.value)
file.status = 'uploading'
file.percentage = evt.percent
}
const handleSuccess: UploadContentProps['onSuccess'] = (
response,
rawFile
) => {
const file = getFile(rawFile)
if (!file) return
file.status = 'success'
file.response = response
props.onSuccess(response, file, uploadFiles.value)
props.onChange(file, uploadFiles.value)
}
const handleStart: UploadContentProps['onStart'] = (file) => {
const uploadFile: UploadFile = {
name: file.name,
percentage: 0,
status: 'ready',
size: file.size,
raw: file,
uid: file.uid,
}
if (props.listType === 'picture-card' || props.listType === 'picture') {
try {
uploadFile.url = URL.createObjectURL(file)
} catch (err: unknown) {
debugWarn(SCOPE, (err as Error).message)
props.onError(err as Error, uploadFile, uploadFiles.value)
}
}
uploadFiles.value.push(uploadFile)
props.onChange(uploadFile, uploadFiles.value)
}
const handleRemove: UploadContentProps['onRemove'] = async (
file,
rawFile // TODO: deprecated in 2.2
): Promise<void> => {
if (rawFile) {
useDeprecated(
{
scope: SCOPE,
from: 'handleRemove second argument',
version: '2.2',
replacement: 'first argument `file`',
ref: 'https://element-plus.org/en-US/component/upload.html#methods',
},
true
)
}
const _file = rawFile || file
const uploadFile = _file instanceof File ? getFile(_file) : _file
if (!uploadFile) throwError(SCOPE, 'file to be removed not found')
const doRemove = (file: UploadFile) => {
abort(file)
const fileList = uploadFiles.value
fileList.splice(fileList.indexOf(file), 1)
props.onRemove(file, fileList)
revokeObjectURL(file)
}
if (props.beforeRemove) {
const before = await props.beforeRemove(uploadFile, uploadFiles.value)
if (before !== false) doRemove(uploadFile)
} else {
doRemove(uploadFile)
}
}
function submit() {
uploadFiles.value
.filter(({ status }) => status === 'ready')
.forEach(({ raw }) => uploadRef.value?.upload(raw))
}
watch(
() => props.listType,
(val) => {
if (val !== 'picture-card' && val !== 'picture') {
return
}
uploadFiles.value = uploadFiles.value.map((file) => {
const { raw, url } = file
if (!url && raw) {
try {
file.url = URL.createObjectURL(raw)
} catch (err: unknown) {
props.onError(err as Error, file, uploadFiles.value)
}
}
return file
})
}
)
watch(
() => props.fileList,
(fileList) => {
for (const file of fileList) {
file.uid = genFileId()
file.status ||= 'success'
}
},
{ immediate: true }
)
return {
abort,
clearFiles,
handleError,
handleProgress,
handleStart,
handleSuccess,
handleRemove,
submit,
uploadFiles,
}
}

View File

@ -1,181 +0,0 @@
import { ref, watch } from 'vue'
import { NOOP } from '@vue/shared'
import { cloneDeep } from 'lodash-unified'
// Inline types
import type {
ListType,
UploadFile,
UploadStatus,
ElFile,
ElUploadProgressEvent,
IUseHandlersProps,
} from './upload.type'
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(
status: UploadStatus[] = ['ready', 'uploading', 'success', 'fail']
) {
uploadFiles.value = uploadFiles.value.filter((row) => {
return !status.includes(row.status)
})
}
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 revokeObjectURL = () => {
if (file.url && file.url.indexOf('blob:') === 0) {
URL.revokeObjectURL(file.url)
}
}
const doRemove = () => {
abort(file)
const fileList = uploadFiles.value
fileList.splice(fileList.indexOf(file), 1)
props.onRemove(file, fileList)
revokeObjectURL()
}
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) => {
const cloneFile = cloneDeep(file)
return {
...cloneFile,
uid: file.uid || genUid(tempIndex++),
status: file.status || 'success',
}
})
},
{
immediate: true,
deep: true,
}
)
return {
abort,
clearFiles,
handleError,
handleProgress,
handleStart,
handleSuccess,
handleRemove,
submit,
uploadFiles,
uploadRef,
}
}

View File

@ -8,3 +8,4 @@ export * from './pagination'
export * from './radio'
export * from './scrollbar'
export * from './tabs'
export * from './upload'

View File

@ -0,0 +1,8 @@
import type { ComputedRef, InjectionKey } from 'vue'
export interface UploadContext {
accept: ComputedRef<string>
}
export const uploadContextKey: InjectionKey<UploadContext> =
Symbol('uploadContextKey')

View File

@ -12,3 +12,4 @@ export type HTMLElementCustomized<T> = HTMLElement & T
export type Nullable<T> = T | null
export type Arrayable<T> = T | T[]
export type Awaitable<T> = Promise<T> | T