import { defineComponent, toRefs, ref, getCurrentInstance } from 'vue';
import { NotificationService } from '../../notification';
import type { IFileResponse } from './upload-types';
import { UploadStatus, UploadProps, uploadProps } from './upload-types';
import { useSelectFiles } from './composables/use-select-files';
import { useUpload } from './composables/use-upload';
import { FileUploader } from './file-uploader';
import { useNamespace } from '../../shared/hooks/use-namespace';
import './upload.scss';
import { createI18nTranslate } from '../../locale/create';
export default defineComponent({
name: 'DUpload',
props: uploadProps,
emits: ['fileDrop', 'fileOver', 'fileSelect', 'deleteUploadedFile', 'update:modelValue'],
setup(props: UploadProps, ctx) {
const app = getCurrentInstance();
const t = createI18nTranslate('DUpload', app);
const {
uploadOptions,
placeholder,
autoUpload,
disabled,
beforeUpload,
droppable,
oneTimeUpload,
modelValue,
multiple,
accept,
webkitdirectory,
limit,
httpRequest,
} = toRefs(props);
const ns = useNamespace('upload');
const inputGroupNs = useNamespace('input-group');
const formControlNs = useNamespace('form-control');
const inputGroupAddOnNs = useNamespace('input-group-addon');
const { triggerSelectFiles, _validateFiles, triggerDropFiles, checkAllFilesSize } = useSelectFiles(t);
const { fileUploaders, addFile, getFullFiles, deleteFile, upload, resetSameNameFiles, removeFiles, _oneTimeUpload, getSameNameFiles } =
useUpload();
const isDropOver = ref(false);
const selectedFiles = ref<File[]>([]);
const alertMsg = (errorMsg?: string) => {
NotificationService.open({
type: 'warning',
content: errorMsg,
});
};
const checkValid = () => {
let totalFileSize = 0;
fileUploaders.value.forEach((fileUploader) => {
totalFileSize += fileUploader.file.size;
const checkResult = _validateFiles(fileUploader.file, accept?.value || '', fileUploader.uploadOptions);
if (checkResult && checkResult.checkError) {
deleteFile(fileUploader.file);
alertMsg(checkResult.errorMsg);
return;
}
});
if (oneTimeUpload.value) {
const checkResult = checkAllFilesSize(totalFileSize, uploadOptions?.value?.maximumSize || 0);
if (checkResult && checkResult.checkError) {
removeFiles();
alertMsg(checkResult.errorMsg);
}
}
};
const onFileOver = (event: boolean) => {
isDropOver.value = event;
ctx.emit('fileOver', event);
};
const deleteUploadedFile = (file: File) => {
const newUploadedFiles = modelValue.value.filter((uploadedFile) => {
return uploadedFile.name !== file.name;
});
ctx.emit('deleteUploadedFile', file);
ctx.emit('update:modelValue', newUploadedFiles);
};
const onDeleteFile = (event: Event, file: File, status: UploadStatus) => {
event?.stopPropagation();
if (status === UploadStatus.uploaded) {
deleteUploadedFile(file);
}
deleteFile(file);
};
const canUpload = () => {
let uploadResult = Promise.resolve(true);
if (beforeUpload?.value) {
const result: boolean | Promise<boolean> = beforeUpload?.value(getFullFiles());
if (typeof result !== 'undefined') {
if (typeof result === 'boolean') {
uploadResult = Promise.resolve(result);
} else {
uploadResult = result;
}
}
}
return uploadResult;
};
const fileUpload = (event?: Event, fileUploader?: FileUploader) => {
if (event) {
event.stopPropagation();
}
canUpload().then((_canUpload) => {
if (!_canUpload) {
removeFiles();
return;
}
if (typeof httpRequest?.value === 'function') {
const files = fileUploaders.value.map((tempFileUploader) => tempFileUploader.file);
httpRequest.value(files);
return;
}
const uploadObservable: Promise<IFileResponse[]> = oneTimeUpload.value ? _oneTimeUpload() : upload(fileUploader);
props.onProgress && props.onProgress(selectedFiles.value, modelValue.value);
uploadObservable
?.then((results: IFileResponse[]) => {
props.onSuccess && props.onSuccess(results);
const newFiles = results.map((result) => result.file);
const newUploadedFiles = [...newFiles, ...modelValue.value];
ctx.emit('update:modelValue', newUploadedFiles);
props.onChange && props.onChange(newFiles, newUploadedFiles);
})
.catch((error: IFileResponse) => {
props.onError && props.onError(error);
props.onChange && props.onChange([error.file], modelValue.value);
});
});
};
const _dealFiles = (promise: Promise<File[]>) => {
resetSameNameFiles();
promise
.then((files) => {
if (limit.value && modelValue.value.length + files.length > limit.value) {
props.onExceed && props.onExceed(files, modelValue.value);
return;
}
props.onChange && props.onChange(files, modelValue.value);
files.forEach((file) => {
if (!multiple.value) {
removeFiles();
}
addFile(file, uploadOptions?.value);
});
checkValid();
const sameNameFiles = getSameNameFiles();
if (uploadOptions?.value && uploadOptions.value.checkSameName && sameNameFiles.length) {
alertMsg(t('getExistSameNameFilesMsg')(sameNameFiles));
}
selectedFiles.value = fileUploaders.value
.filter((fileUploader) => fileUploader.status === UploadStatus.preLoad)
.map((fileUploader) => fileUploader.file);
ctx.emit('fileSelect', selectedFiles.value);
if (autoUpload.value) {
fileUpload();
}
})
.catch((error: Error) => {
alertMsg(error.message);
});
};
const handleClick = () => {
if (disabled.value) {
return;
}
_dealFiles(
triggerSelectFiles({
accept: accept?.value,
multiple: multiple.value,
webkitdirectory: webkitdirectory.value,
})
);
};
const onFileDrop = (files: File[]) => {
isDropOver.value = false;
_dealFiles(triggerDropFiles(files));
ctx.emit('fileDrop', files);
};
const submit = (event?: Event, fileUploader?: FileUploader) => {
fileUpload(event, fileUploader);
};
const clearFiles = (event: MouseEvent) => {
fileUploaders.value.forEach((fileUploader) => {
onDeleteFile(event, fileUploader.file, fileUploader.status);
});
};
const clickSelectedFile = (event: Event, file: File) => {
event?.stopPropagation();
props.onPreview && props.onPreview(file);
};
ctx.expose({ submit, clearFiles });
return () => (
<div>
<div
class={ns.b()}
v-file-drop={{ droppable, isSingle: !multiple, onFileDrop, onFileOver }}
style={`border: ${isDropOver.value ? '1px solid #15bf15' : '0'}`}>
{ctx.slots.default?.() ? (
<div onClick={handleClick}>{ctx.slots.default()}</div>
) : (
<div class={[inputGroupNs.b(), disabled.value ? 'disabled' : '']} onClick={handleClick}>
{fileUploaders.value.length === 0 && (
<div class={[formControlNs.b(), ns.e('placeholder')]}>{placeholder.value || t('placeholder')}</div>
)}
{fileUploaders.value.length > 0 && (
<ul class={[formControlNs.b(), ns.e('files-list')]}>
{fileUploaders.value.map((fileUploader, index) => (
<li
key={index}
class={[ns.e('file-item'), ns.e('file-tag')]}
style="display: inline-block;"
title={fileUploader.file.name}
onClick={(event: Event) => clickSelectedFile(event, fileUploader.file)}>
<span class={[ns.e('filename'), fileUploader.status === UploadStatus.failed ? ns.m('failed-color') : '']}>
{fileUploader.file.name}
</span>
<d-icon
name="close"
class={[
fileUploader?.status === UploadStatus.failed ? ns.e('delete-file-button') : '',
fileUploader?.status === UploadStatus.uploading || fileUploader?.status === UploadStatus.uploaded
? ns.e('delete')
: '',
]}
onClick={(event: Event) => onDeleteFile(event, fileUploader.file, fileUploader.status)}
/>
{fileUploader.status === UploadStatus.uploading && (
<div class={['icon', ns.e('progress')]}>
<d-progress
isCircle={true}
percentage={fileUploader.percentage}
barbgcolor="#50D4AB"
strokeWidth={8}
showContent={false}></d-progress>
</div>
)}
{fileUploader.status === UploadStatus.failed && <d-icon name="running" onClick={fileUpload} />}
{fileUploader.status === UploadStatus.uploaded && <d-icon name="right" color="#50d4ab" />}
</li>
))}
</ul>
)}
<span class={inputGroupAddOnNs.b()}>
<d-icon name="more-operate" color="#252b3a" />
</span>
</div>
)}
</div>
<div>
{ctx.slots['uploaded-files']?.({
uploadedFiles: modelValue.value,
deleteFile: deleteUploadedFile,
})}
</div>
</div>
);
},
});