✨ File upload component added
This commit is contained in:
parent
16f0617df2
commit
1acd65a704
371
src/components/file/VFileUpload.vue
Normal file
371
src/components/file/VFileUpload.vue
Normal file
|
@ -0,0 +1,371 @@
|
|||
<script setup lang="ts">
|
||||
import FileUpload from 'primevue/fileupload';
|
||||
import VHint from '../hint/VHint.vue';
|
||||
import VLabel from '../label/VLabel.vue';
|
||||
import VButton from '../button/VButton.vue';
|
||||
import VButtonGroup from '../button/VButtonGroup.vue';
|
||||
import VFile from './VFile.vue';
|
||||
import VMessage from '../message/VMessage.vue';
|
||||
import type IVFileUpload from './IVFileUpload.type';
|
||||
import type { FileUploadErrorEvent, FileUploadProgressEvent, FileUploadRemoveEvent, FileUploadSelectEvent, FileUploadUploadEvent } from 'primevue/fileupload';
|
||||
import { computed, useId, ref, onMounted } from 'vue';
|
||||
import styles from '@visua/typography.module.css';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const fileUploadRef = ref();
|
||||
const fileProgressMap = ref<Record<string, number>>({});
|
||||
|
||||
defineExpose({
|
||||
upload: () => fileUploadRef.value.upload()
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<IVFileUpload>(), {
|
||||
id: () => useId(),
|
||||
label: 'Ajouter un fichier',
|
||||
validMessage: '',
|
||||
hint: '',
|
||||
disabled: false,
|
||||
invalidFileSizeMessage: 'Taille de fichier invalide, la taille du fichier doit être plus petite que {1.}',
|
||||
invalidFileLimitMessage: 'Nombre maximal des fichiers atteint, le nombre maximal est {0.}',
|
||||
invalidFileTypeMessage: 'Type de fichier invalide, le type de fichier {0} n\'est pas pris en charge',
|
||||
accept: undefined,
|
||||
chooseLabel: 'Choisir',
|
||||
name: undefined,
|
||||
url: undefined,
|
||||
maxFileSize: undefined,
|
||||
fileLimit: undefined,
|
||||
withCredentials: false,
|
||||
previewWidth: 50,
|
||||
uploadLabel: 'Téléverser',
|
||||
cancelLabel: 'Annuler',
|
||||
showCancelButton: true,
|
||||
showUploadButton: true,
|
||||
advanced: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'select',
|
||||
'before-upload',
|
||||
'progress',
|
||||
'upload',
|
||||
'error',
|
||||
'before-send',
|
||||
'clear',
|
||||
'remove',
|
||||
'removeUploadFile',
|
||||
'uploader',
|
||||
]);
|
||||
const fileSelected = ref(false);
|
||||
const hasActiveError = ref(false);
|
||||
|
||||
const lastSelectedFile = ref<File | null>(null);
|
||||
|
||||
const handleSelect = (event: FileUploadSelectEvent) => {
|
||||
emit('select', event);
|
||||
|
||||
if (!props.advanced && event.files.length > 0 && fileUploadRef.value) {
|
||||
fileUploadRef.value.clear();
|
||||
fileUploadRef.value.files.push(event.files[event.files.length - 1]);
|
||||
}
|
||||
fileSelected.value = true;
|
||||
};
|
||||
|
||||
|
||||
const handleClear = () => {
|
||||
emit('clear');
|
||||
fileSelected.value = false;
|
||||
lastSelectedFile.value = null;
|
||||
hasActiveError.value = false;
|
||||
};
|
||||
|
||||
const handleUpload = (event: FileUploadUploadEvent) => {
|
||||
totalSize.value = 0;
|
||||
totalSizePercent.value = 100;
|
||||
emit('upload', event);
|
||||
hasActiveError.value = false;
|
||||
};
|
||||
|
||||
|
||||
const handleRemove = (event: FileUploadRemoveEvent) => {
|
||||
emit('remove', event);
|
||||
fileSelected.value = false;
|
||||
lastSelectedFile.value = null;
|
||||
hasActiveError.value =false;
|
||||
};
|
||||
|
||||
const handleProgress = (event: FileUploadProgressEvent) => {
|
||||
emit('progress', event);
|
||||
|
||||
const files = fileUploadRef.value?.files || [];
|
||||
files.forEach((file: File) => {
|
||||
fileProgressMap.value[file.name] = event.progress;
|
||||
});
|
||||
};
|
||||
|
||||
const handleError = (event: FileUploadErrorEvent) => {
|
||||
emit('error', event);
|
||||
hasActiveError.value = true;
|
||||
}
|
||||
|
||||
const error = computed(() => hasActiveError.value);
|
||||
|
||||
|
||||
const isSimpleAndEmpty = computed(() => {
|
||||
return !props.advanced && !fileSelected.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const currentFiles = fileUploadRef.value?.files || [];
|
||||
fileSelected.value = currentFiles.length > 0;
|
||||
});
|
||||
|
||||
const totalSize = ref(0);
|
||||
const totalSizePercent = ref(0);
|
||||
|
||||
const uploadEvent = (callback: () => void) => {
|
||||
totalSizePercent.value = totalSize.value / 10;
|
||||
callback();
|
||||
fileUploadRef.value.upload();
|
||||
};
|
||||
|
||||
const padding = computed(() => props.advanced ? '1.125rem' : '0rem')
|
||||
const borderColor = computed(() => props.advanced ? 'var(--border-default-grey)' : 'transparent');
|
||||
|
||||
const getLastFileSelected = (files: File[]) => {
|
||||
const indexOfLastFileSelected = files.length - 1;
|
||||
return files[indexOfLastFileSelected];
|
||||
}
|
||||
|
||||
const labelState = computed(() => {
|
||||
if(!error.value && !props.disabled) return 'default';
|
||||
else if(error.value && !props.disabled) return 'error';
|
||||
else return undefined
|
||||
})
|
||||
|
||||
type MessageType = 'alert' | 'warning' | 'success' | 'info';
|
||||
|
||||
const globalStatusMessage = computed<{
|
||||
type: MessageType;
|
||||
title: string;
|
||||
} | null>(() => {
|
||||
const files = fileUploadRef.value?.files || [];
|
||||
const uploaded = fileUploadRef.value?.uploadedFiles || [];
|
||||
|
||||
const hasError = files.some((f: { status: string; }) => f.status === 'error');
|
||||
const hasPending = files.length > 0;
|
||||
|
||||
if (hasError) return { type: 'alert', title: 'Erreur lors du téléversement' };
|
||||
if (hasPending) return { type: 'warning', title: 'En attente de téléversement' };
|
||||
if (uploaded.length > 0) return { type: 'success', title: 'Tous les fichiers ont été téléversés' };
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<VLabel
|
||||
:for="`file-upload-${props.id}`"
|
||||
:label="props.label"
|
||||
:disabled="props.disabled"
|
||||
:type="labelState"
|
||||
:hint="props.hint"
|
||||
/>
|
||||
<FileUpload
|
||||
ref="fileUploadRef"
|
||||
:id="`file-upload-${props.id}`"
|
||||
:multiple="props.advanced"
|
||||
:invalid-file-limit-message="props.invalidFileLimitMessage"
|
||||
:invalid-file-size-message="props.invalidFileSizeMessage"
|
||||
:invalid-file-type-message="props.invalidFileTypeMessage"
|
||||
mode="advanced"
|
||||
:accept="props.accept"
|
||||
:disabled="props.disabled"
|
||||
v-bind="$attrs"
|
||||
:auto="!props.advanced"
|
||||
:choose-label="props.chooseLabel"
|
||||
:upload-label="props.uploadLabel"
|
||||
:cancel-label="props.cancelLabel"
|
||||
:name="props.name"
|
||||
:url="props.url"
|
||||
:max-file-size="props.maxFileSize"
|
||||
:file-limit="props.fileLimit"
|
||||
:with-credentials="props.withCredentials"
|
||||
:preview-width="props.previewWidth"
|
||||
:show-cancel-button="props.showCancelButton"
|
||||
:show-upload-button="props.showUploadButton"
|
||||
@select="handleSelect($event)"
|
||||
@before-send="emit('before-send', $event)"
|
||||
@progress="handleProgress($event)"
|
||||
@before-upload="emit('before-upload', $event)"
|
||||
@uploader="emit('uploader', $event)"
|
||||
@upload="handleUpload($event)"
|
||||
@error="handleError($event)"
|
||||
@clear="handleClear()"
|
||||
@remove="handleRemove($event)"
|
||||
@remove-uploaded-file="emit('removeUploadFile', $event)"
|
||||
class="p-fileupload"
|
||||
>
|
||||
<template #header="slotProps">
|
||||
<div v-if="props.advanced" class="advanced-fileupload-header">
|
||||
<VButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
label: 'Parcourir...',
|
||||
onClick: slotProps.chooseCallback,
|
||||
title: 'parcourir les fichiers',
|
||||
},
|
||||
{
|
||||
label: 'Téléverser',
|
||||
icon: 'ri-upload-cloud-line',
|
||||
onClick: () => uploadEvent(slotProps.uploadCallback),
|
||||
secondary: true,
|
||||
disabled: !slotProps.files || slotProps.files.length === 0,
|
||||
title: 'televerser les fichiers'
|
||||
},
|
||||
{
|
||||
label: 'Supprimer',
|
||||
tertiary: true,
|
||||
onClick: slotProps.clearCallback,
|
||||
disabled: !slotProps.files || slotProps.files.length === 0,
|
||||
title: 'suprimer les fichiers'
|
||||
}
|
||||
]"
|
||||
size="sm"
|
||||
:disabled="props.disabled"
|
||||
inlineLayoutWhen="always"
|
||||
title="boutons de gestion des fichiers"
|
||||
/>
|
||||
<VMessage
|
||||
v-if="globalStatusMessage"
|
||||
:type="globalStatusMessage.type"
|
||||
:title="globalStatusMessage.title"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="simple-fileupload-header">
|
||||
<VButton label="Parcourir..." :disabled="props.disabled" size="sm" @click="slotProps.chooseCallback" title="parcourir les fichiers"/>
|
||||
<span
|
||||
v-if="isSimpleAndEmpty"
|
||||
:class="[styles['text-body-SM-detail-text-Regular']]"
|
||||
>
|
||||
Aucun fichier sélectionné
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content="slotProps" v-if="!isSimpleAndEmpty">
|
||||
<div v-if="props.advanced" class="advanced-container-content">
|
||||
<div class="fileupload-content">
|
||||
<div v-if="slotProps.files.length > 0">
|
||||
<div class="file-area">
|
||||
<VFile
|
||||
v-for="(file, index) in slotProps.files"
|
||||
:key="file.name + file.type + file.size"
|
||||
:file="file"
|
||||
:index="index"
|
||||
:remove-file-callback="slotProps.removeFileCallback"
|
||||
:progress="fileProgressMap[file.name] || 0"
|
||||
status="pending"
|
||||
advanced
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="slotProps.uploadedFiles.length > 0" class="fileupload-content">
|
||||
<div class="file-area">
|
||||
<VFile
|
||||
v-for="(file, index) in slotProps.uploadedFiles"
|
||||
:key="file.name + file.type + file.size"
|
||||
:file="file"
|
||||
:index="index"
|
||||
:remove-file-callback="slotProps.removeFileCallback"
|
||||
:progress="100"
|
||||
status="completed"
|
||||
advanced
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="slotProps.files.length > 0" class="simple-container-content">
|
||||
<VFile
|
||||
:file="getLastFileSelected(slotProps.files)"
|
||||
:remove-file-callback="slotProps.removeFileCallback"
|
||||
/>
|
||||
</div>
|
||||
<VHint
|
||||
v-for="message of slotProps.messages"
|
||||
:key="message"
|
||||
:title="message"
|
||||
type="alert"
|
||||
icon
|
||||
/>
|
||||
</template>
|
||||
<template #empty v-if="props.advanced">
|
||||
<div class="fileupload-empty" v-if="props.advanced">
|
||||
<i class="ri-upload-cloud-line upload-cloud-icon"></i>
|
||||
<p>Glissez-déposez les fichiers ici pour les téléverser.</p>
|
||||
</div>
|
||||
</template>
|
||||
</FileUpload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
*{
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.p-fileupload{
|
||||
width: 100%;
|
||||
--p-fileupload-border-color: v-bind(borderColor);
|
||||
padding: v-bind(padding);
|
||||
}
|
||||
|
||||
.simple-fileupload-header{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--p-fileupload-basic-gap);
|
||||
align-items: baseline;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.advanced-fileupload-header{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-fileupload-content-gap);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.fileupload-empty{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-cloud-icon{
|
||||
font-size: 5rem;
|
||||
color: var(--border-contrast-grey);
|
||||
}
|
||||
|
||||
.file-area{
|
||||
margin-top: 0.75rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 0.25rem;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user