vuecssuiant-designantdreactantantd-vueenterprisefrontendui-designvue-antdvue-antd-uivue3vuecomponent
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
8.5 KiB
317 lines
8.5 KiB
import defaultRequest from './request'; |
|
import getUid from './uid'; |
|
import attrAccept from './attr-accept'; |
|
import traverseFileTree from './traverseFileTree'; |
|
import type { |
|
RcFile, |
|
UploadProgressEvent, |
|
UploadRequestError, |
|
BeforeUploadFileType, |
|
} from './interface'; |
|
import { uploadProps } from './interface'; |
|
import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'; |
|
import type { ChangeEvent } from '../_util/EventInterface'; |
|
import pickAttrs from '../_util/pickAttrs'; |
|
import partition from 'lodash-es/partition'; |
|
|
|
interface ParsedFileInfo { |
|
origin: RcFile; |
|
action: string; |
|
data: Record<string, unknown>; |
|
parsedFile: RcFile; |
|
} |
|
export default defineComponent({ |
|
name: 'AjaxUploader', |
|
inheritAttrs: false, |
|
props: uploadProps(), |
|
setup(props, { slots, attrs, expose }) { |
|
const uid = ref(getUid()); |
|
const reqs: any = {}; |
|
|
|
const fileInput = ref<HTMLInputElement>(); |
|
|
|
let isMounted = false; |
|
|
|
/** |
|
* Process file before upload. When all the file is ready, we start upload. |
|
*/ |
|
const processFile = async (file: RcFile, fileList: RcFile[]): Promise<ParsedFileInfo> => { |
|
const { beforeUpload } = props; |
|
|
|
let transformedFile: BeforeUploadFileType | void = file; |
|
if (beforeUpload) { |
|
try { |
|
transformedFile = await beforeUpload(file, fileList); |
|
} catch (e) { |
|
// Rejection will also trade as false |
|
transformedFile = false; |
|
} |
|
if (transformedFile === false) { |
|
return { |
|
origin: file, |
|
parsedFile: null, |
|
action: null, |
|
data: null, |
|
}; |
|
} |
|
} |
|
|
|
// Get latest action |
|
const { action } = props; |
|
let mergedAction: string; |
|
if (typeof action === 'function') { |
|
mergedAction = await action(file); |
|
} else { |
|
mergedAction = action; |
|
} |
|
|
|
// Get latest data |
|
const { data } = props; |
|
let mergedData: Record<string, unknown>; |
|
if (typeof data === 'function') { |
|
mergedData = await data(file); |
|
} else { |
|
mergedData = data; |
|
} |
|
|
|
const parsedData = |
|
// string type is from legacy `transformFile`. |
|
// Not sure if this will work since no related test case works with it |
|
(typeof transformedFile === 'object' || typeof transformedFile === 'string') && |
|
transformedFile |
|
? transformedFile |
|
: file; |
|
|
|
let parsedFile: File; |
|
if (parsedData instanceof File) { |
|
parsedFile = parsedData; |
|
} else { |
|
parsedFile = new File([parsedData], file.name, { type: file.type }); |
|
} |
|
|
|
const mergedParsedFile: RcFile = parsedFile as RcFile; |
|
mergedParsedFile.uid = file.uid; |
|
|
|
return { |
|
origin: file, |
|
data: mergedData, |
|
parsedFile: mergedParsedFile, |
|
action: mergedAction, |
|
}; |
|
}; |
|
|
|
const post = ({ data, origin, action, parsedFile }: ParsedFileInfo) => { |
|
if (!isMounted) { |
|
return; |
|
} |
|
|
|
const { onStart, customRequest, name, headers, withCredentials, method } = props; |
|
|
|
const { uid } = origin; |
|
const request = customRequest || defaultRequest; |
|
|
|
const requestOption = { |
|
action, |
|
filename: name, |
|
data, |
|
file: parsedFile, |
|
headers, |
|
withCredentials, |
|
method: method || 'post', |
|
onProgress: (e: UploadProgressEvent) => { |
|
const { onProgress } = props; |
|
onProgress?.(e, parsedFile); |
|
}, |
|
onSuccess: (ret: any, xhr: XMLHttpRequest) => { |
|
const { onSuccess } = props; |
|
onSuccess?.(ret, parsedFile, xhr); |
|
|
|
delete reqs[uid]; |
|
}, |
|
onError: (err: UploadRequestError, ret: any) => { |
|
const { onError } = props; |
|
onError?.(err, ret, parsedFile); |
|
|
|
delete reqs[uid]; |
|
}, |
|
}; |
|
|
|
onStart(origin); |
|
reqs[uid] = request(requestOption); |
|
}; |
|
|
|
const reset = () => { |
|
uid.value = getUid(); |
|
}; |
|
|
|
const abort = (file?: any) => { |
|
if (file) { |
|
const uid = file.uid ? file.uid : file; |
|
if (reqs[uid] && reqs[uid].abort) { |
|
reqs[uid].abort(); |
|
} |
|
delete reqs[uid]; |
|
} else { |
|
Object.keys(reqs).forEach(uid => { |
|
if (reqs[uid] && reqs[uid].abort) { |
|
reqs[uid].abort(); |
|
} |
|
delete reqs[uid]; |
|
}); |
|
} |
|
}; |
|
|
|
onMounted(() => { |
|
isMounted = true; |
|
}); |
|
|
|
onBeforeUnmount(() => { |
|
isMounted = false; |
|
abort(); |
|
}); |
|
const uploadFiles = (files: File[]) => { |
|
const originFiles = [...files] as RcFile[]; |
|
const postFiles = originFiles.map((file: RcFile & { uid?: string }) => { |
|
// eslint-disable-next-line no-param-reassign |
|
file.uid = getUid(); |
|
return processFile(file, originFiles); |
|
}); |
|
|
|
// Batch upload files |
|
Promise.all(postFiles).then(fileList => { |
|
const { onBatchStart } = props; |
|
|
|
onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile }))); |
|
|
|
fileList |
|
.filter(file => file.parsedFile !== null) |
|
.forEach(file => { |
|
post(file); |
|
}); |
|
}); |
|
}; |
|
|
|
const onChange = (e: ChangeEvent) => { |
|
const { accept, directory } = props; |
|
const { files } = e.target as any; |
|
const acceptedFiles = [...files].filter( |
|
(file: RcFile) => !directory || attrAccept(file, accept), |
|
); |
|
uploadFiles(acceptedFiles); |
|
reset(); |
|
}; |
|
|
|
const onClick = (e: MouseEvent | KeyboardEvent) => { |
|
const el = fileInput.value; |
|
if (!el) { |
|
return; |
|
} |
|
const { onClick } = props; |
|
// TODO |
|
// if (children && (children as any).type === 'button') { |
|
// const parent = el.parentNode as HTMLInputElement; |
|
// parent.focus(); |
|
// parent.querySelector('button').blur(); |
|
// } |
|
el.click(); |
|
if (onClick) { |
|
onClick(e); |
|
} |
|
}; |
|
|
|
const onKeyDown = (e: KeyboardEvent) => { |
|
if (e.key === 'Enter') { |
|
onClick(e); |
|
} |
|
}; |
|
|
|
const onFileDrop = (e: DragEvent) => { |
|
const { multiple } = props; |
|
|
|
e.preventDefault(); |
|
|
|
if (e.type === 'dragover') { |
|
return; |
|
} |
|
|
|
if (props.directory) { |
|
traverseFileTree( |
|
Array.prototype.slice.call(e.dataTransfer.items), |
|
uploadFiles, |
|
(_file: RcFile) => attrAccept(_file, props.accept), |
|
); |
|
} else { |
|
const files: [RcFile[], RcFile[]] = partition( |
|
Array.prototype.slice.call(e.dataTransfer.files), |
|
(file: RcFile) => attrAccept(file, props.accept), |
|
); |
|
let successFiles = files[0]; |
|
const errorFiles = files[1]; |
|
if (multiple === false) { |
|
successFiles = successFiles.slice(0, 1); |
|
} |
|
|
|
uploadFiles(successFiles); |
|
if (errorFiles.length && props.onReject) props.onReject(errorFiles); |
|
} |
|
}; |
|
expose({ |
|
abort, |
|
}); |
|
return () => { |
|
const { |
|
componentTag: Tag, |
|
prefixCls, |
|
disabled, |
|
id, |
|
multiple, |
|
accept, |
|
capture, |
|
directory, |
|
openFileDialogOnClick, |
|
onMouseenter, |
|
onMouseleave, |
|
...otherProps |
|
} = props; |
|
const cls = { |
|
[prefixCls]: true, |
|
[`${prefixCls}-disabled`]: disabled, |
|
[attrs.class as string]: !!attrs.class, |
|
}; |
|
// because input don't have directory/webkitdirectory type declaration |
|
const dirProps: any = directory |
|
? { directory: 'directory', webkitdirectory: 'webkitdirectory' } |
|
: {}; |
|
const events = disabled |
|
? {} |
|
: { |
|
onClick: openFileDialogOnClick ? onClick : () => {}, |
|
onKeydown: openFileDialogOnClick ? onKeyDown : () => {}, |
|
onMouseenter, |
|
onMouseleave, |
|
onDrop: onFileDrop, |
|
onDragover: onFileDrop, |
|
tabindex: '0', |
|
}; |
|
return ( |
|
<Tag {...events} class={cls} role="button" style={attrs.style}> |
|
<input |
|
{...pickAttrs(otherProps, { aria: true, data: true })} |
|
id={id} |
|
type="file" |
|
ref={fileInput} |
|
onClick={e => e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948 |
|
key={uid.value} |
|
style={{ display: 'none' }} |
|
accept={accept} |
|
{...dirProps} |
|
multiple={multiple} |
|
onChange={onChange} |
|
{...(capture != null ? { capture } : {})} |
|
/> |
|
{slots.default?.()} |
|
</Tag> |
|
); |
|
}; |
|
}, |
|
});
|
|
|