refactor: vc-upload
parent
3cf5d4fa43
commit
b0c3677d50
|
@ -0,0 +1,313 @@
|
|||
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';
|
||||
|
||||
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 {
|
||||
let files = [...e.dataTransfer.files].filter((file: RcFile) =>
|
||||
attrAccept(file, props.accept),
|
||||
);
|
||||
|
||||
if (multiple === false) {
|
||||
files = files.slice(0, 1);
|
||||
}
|
||||
|
||||
uploadFiles(files);
|
||||
}
|
||||
};
|
||||
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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
import { defineComponent, ref } from 'vue';
|
||||
import { initDefaultProps } from '../_util/props-util';
|
||||
import AjaxUpload from './AjaxUploader';
|
||||
import type { RcFile } from './interface';
|
||||
import { uploadProps } from './interface';
|
||||
|
||||
function empty() {}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Upload',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(uploadProps(), {
|
||||
componentTag: 'span',
|
||||
prefixCls: 'rc-upload',
|
||||
data: {},
|
||||
headers: {},
|
||||
name: 'file',
|
||||
multipart: false,
|
||||
onStart: empty,
|
||||
onError: empty,
|
||||
onSuccess: empty,
|
||||
multiple: false,
|
||||
beforeUpload: null,
|
||||
customRequest: null,
|
||||
withCredentials: false,
|
||||
openFileDialogOnClick: true,
|
||||
}),
|
||||
setup(props, { slots, attrs, expose }) {
|
||||
const uploader = ref();
|
||||
|
||||
const abort = (file: RcFile) => {
|
||||
uploader.value?.abort(file);
|
||||
};
|
||||
expose({
|
||||
abort,
|
||||
});
|
||||
return () => {
|
||||
return <AjaxUpload {...props} {...attrs} v-slots={slots} ref={uploader} />;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import { warning } from '../vc-util/warning';
|
||||
import type { RcFile } from './interface';
|
||||
|
||||
export default (file: RcFile, acceptedFiles: string | string[]) => {
|
||||
if (file && acceptedFiles) {
|
||||
const acceptedFilesArray = Array.isArray(acceptedFiles)
|
||||
? acceptedFiles
|
||||
: acceptedFiles.split(',');
|
||||
const fileName = file.name || '';
|
||||
const mimeType = file.type || '';
|
||||
const baseMimeType = mimeType.replace(/\/.*$/, '');
|
||||
|
||||
return acceptedFilesArray.some(type => {
|
||||
const validType = type.trim();
|
||||
// This is something like */*,* allow all files
|
||||
if (/^\*(\/\*)?$/.test(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// like .jpg, .png
|
||||
if (validType.charAt(0) === '.') {
|
||||
const lowerFileName = fileName.toLowerCase();
|
||||
const lowerType = validType.toLowerCase();
|
||||
|
||||
let affixList = [lowerType];
|
||||
if (lowerType === '.jpg' || lowerType === '.jpeg') {
|
||||
affixList = ['.jpg', '.jpeg'];
|
||||
}
|
||||
|
||||
return affixList.some(affix => lowerFileName.endsWith(affix));
|
||||
}
|
||||
|
||||
// This is something like a image/* mime type
|
||||
if (/\/\*$/.test(validType)) {
|
||||
return baseMimeType === validType.replace(/\/.*$/, '');
|
||||
}
|
||||
|
||||
// Full match
|
||||
if (mimeType === validType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Invalidate type should skip
|
||||
if (/^\w+$/.test(validType)) {
|
||||
warning(false, `Upload takes an invalidate 'accept' type '${validType}'.Skip for check.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
// rc-upload 4.3.3
|
||||
import Upload from './Upload';
|
||||
import { UploadProps } from './interface';
|
||||
|
||||
export { UploadProps };
|
||||
|
||||
export default Upload;
|
|
@ -0,0 +1,82 @@
|
|||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export type BeforeUploadFileType = File | Blob | boolean | string;
|
||||
|
||||
export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
|
||||
|
||||
export const uploadProps = () => {
|
||||
return {
|
||||
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
|
||||
multipart: Boolean,
|
||||
name: String,
|
||||
disabled: Boolean,
|
||||
componentTag: String as PropType<any>,
|
||||
action: [String, Function] as PropType<Action>,
|
||||
method: String as PropType<UploadRequestMethod>,
|
||||
directory: Boolean,
|
||||
data: [Object, Function] as PropType<
|
||||
Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>)
|
||||
>,
|
||||
headers: Object as PropType<UploadRequestHeader>,
|
||||
accept: String,
|
||||
multiple: Boolean,
|
||||
onBatchStart: Function as PropType<
|
||||
(fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[]) => void
|
||||
>,
|
||||
onStart: Function as PropType<(file: RcFile) => void>,
|
||||
onError: Function as PropType<
|
||||
(error: Error, ret: Record<string, unknown>, file: RcFile) => void
|
||||
>,
|
||||
onSuccess: Function as PropType<
|
||||
(response: Record<string, unknown>, file: RcFile, xhr: XMLHttpRequest) => void
|
||||
>,
|
||||
onProgress: Function as PropType<(event: UploadProgressEvent, file: RcFile) => void>,
|
||||
beforeUpload: Function as PropType<
|
||||
(
|
||||
file: RcFile,
|
||||
FileList: RcFile[],
|
||||
) => BeforeUploadFileType | Promise<void | BeforeUploadFileType>
|
||||
>,
|
||||
customRequest: Function as PropType<(option: UploadRequestOption) => void>,
|
||||
withCredentials: Boolean,
|
||||
openFileDialogOnClick: Boolean,
|
||||
prefixCls: String,
|
||||
id: String,
|
||||
onMouseenter: Function as PropType<(e: MouseEvent) => void>,
|
||||
onMouseleave: Function as PropType<(e: MouseEvent) => void>,
|
||||
onClick: Function as PropType<(e: MouseEvent | KeyboardEvent) => void>,
|
||||
};
|
||||
};
|
||||
|
||||
export type UploadProps = Partial<ExtractPropTypes<ReturnType<typeof uploadProps>>>;
|
||||
|
||||
export interface UploadProgressEvent extends Partial<ProgressEvent> {
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
export type UploadRequestMethod = 'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch';
|
||||
|
||||
export type UploadRequestHeader = Record<string, string>;
|
||||
|
||||
export interface UploadRequestError extends Error {
|
||||
status?: number;
|
||||
method?: UploadRequestMethod;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface UploadRequestOption<T = any> {
|
||||
onProgress?: (event: UploadProgressEvent) => void;
|
||||
onError?: (event: UploadRequestError | ProgressEvent, body?: T) => void;
|
||||
onSuccess?: (body: T, xhr?: XMLHttpRequest) => void;
|
||||
data?: Record<string, unknown>;
|
||||
filename?: string;
|
||||
file: Exclude<BeforeUploadFileType, File | boolean> | RcFile;
|
||||
withCredentials?: boolean;
|
||||
action: string;
|
||||
headers?: UploadRequestHeader;
|
||||
method: UploadRequestMethod;
|
||||
}
|
||||
|
||||
export interface RcFile extends File {
|
||||
uid: string;
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import type { UploadRequestOption, UploadRequestError, UploadProgressEvent } from './interface';
|
||||
|
||||
function getError(option: UploadRequestOption, xhr: XMLHttpRequest) {
|
||||
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
|
||||
const err = new Error(msg) as UploadRequestError;
|
||||
err.status = xhr.status;
|
||||
err.method = option.method;
|
||||
err.url = option.action;
|
||||
return err;
|
||||
}
|
||||
|
||||
function getBody(xhr: XMLHttpRequest) {
|
||||
const text = xhr.responseText || xhr.response;
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export default function upload(option: UploadRequestOption) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
if (option.onProgress && xhr.upload) {
|
||||
xhr.upload.onprogress = function progress(e: UploadProgressEvent) {
|
||||
if (e.total > 0) {
|
||||
e.percent = (e.loaded / e.total) * 100;
|
||||
}
|
||||
option.onProgress(e);
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const formData = new FormData();
|
||||
|
||||
if (option.data) {
|
||||
Object.keys(option.data).forEach(key => {
|
||||
const value = option.data[key];
|
||||
// support key-value array data
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => {
|
||||
// { list: [ 11, 22 ] }
|
||||
// formData.append('list[]', 11);
|
||||
formData.append(`${key}[]`, item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(key, value as string | Blob);
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (option.file instanceof Blob) {
|
||||
formData.append(option.filename, option.file, (option.file as any).name);
|
||||
} else {
|
||||
formData.append(option.filename, option.file);
|
||||
}
|
||||
|
||||
xhr.onerror = function error(e) {
|
||||
option.onError(e);
|
||||
};
|
||||
|
||||
xhr.onload = function onload() {
|
||||
// allow success when 2xx status
|
||||
// see https://github.com/react-component/upload/issues/34
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
return option.onError(getError(option, xhr), getBody(xhr));
|
||||
}
|
||||
|
||||
return option.onSuccess(getBody(xhr), xhr);
|
||||
};
|
||||
|
||||
xhr.open(option.method, option.action, true);
|
||||
|
||||
// Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
|
||||
if (option.withCredentials && 'withCredentials' in xhr) {
|
||||
xhr.withCredentials = true;
|
||||
}
|
||||
|
||||
const headers = option.headers || {};
|
||||
|
||||
// when set headers['X-Requested-With'] = null , can close default XHR header
|
||||
// see https://github.com/react-component/upload/issues/33
|
||||
if (headers['X-Requested-With'] !== null) {
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
}
|
||||
|
||||
Object.keys(headers).forEach(h => {
|
||||
if (headers[h] !== null) {
|
||||
xhr.setRequestHeader(h, headers[h]);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.send(formData);
|
||||
|
||||
return {
|
||||
abort() {
|
||||
xhr.abort();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import type { RcFile } from './interface';
|
||||
|
||||
interface InternalDataTransferItem extends DataTransferItem {
|
||||
isFile: boolean;
|
||||
file: (cd: (file: RcFile & { webkitRelativePath?: string }) => void) => void;
|
||||
createReader: () => any;
|
||||
fullPath: string;
|
||||
isDirectory: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function loopFiles(item: InternalDataTransferItem, callback) {
|
||||
const dirReader = item.createReader();
|
||||
let fileList = [];
|
||||
|
||||
function sequence() {
|
||||
dirReader.readEntries((entries: InternalDataTransferItem[]) => {
|
||||
const entryList = Array.prototype.slice.apply(entries);
|
||||
fileList = fileList.concat(entryList);
|
||||
|
||||
// Check if all the file has been viewed
|
||||
const isFinished = !entryList.length;
|
||||
|
||||
if (isFinished) {
|
||||
callback(fileList);
|
||||
} else {
|
||||
sequence();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sequence();
|
||||
}
|
||||
|
||||
const traverseFileTree = (files: InternalDataTransferItem[], callback, isAccepted) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const _traverseFileTree = (item: InternalDataTransferItem, path?: string) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.path = path || '';
|
||||
if (item.isFile) {
|
||||
item.file(file => {
|
||||
if (isAccepted(file)) {
|
||||
// https://github.com/ant-design/ant-design/issues/16426
|
||||
if (item.fullPath && !file.webkitRelativePath) {
|
||||
Object.defineProperties(file, {
|
||||
webkitRelativePath: {
|
||||
writable: true,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
(file as any).webkitRelativePath = item.fullPath.replace(/^\//, '');
|
||||
Object.defineProperties(file, {
|
||||
webkitRelativePath: {
|
||||
writable: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
callback([file]);
|
||||
}
|
||||
});
|
||||
} else if (item.isDirectory) {
|
||||
loopFiles(item, (entries: InternalDataTransferItem[]) => {
|
||||
entries.forEach(entryItem => {
|
||||
_traverseFileTree(entryItem, `${path}${item.name}/`);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
files.forEach(file => {
|
||||
_traverseFileTree(file.webkitGetAsEntry() as any);
|
||||
});
|
||||
};
|
||||
|
||||
export default traverseFileTree;
|
|
@ -0,0 +1,7 @@
|
|||
const now = +new Date();
|
||||
let index = 0;
|
||||
|
||||
export default function uid() {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
return `vc-upload-${now}-${++index}`;
|
||||
}
|
Loading…
Reference in New Issue