visua-vue/src/components/file/VFileUpload.vue
Paul Valerie GOMA f15fdb8e18 🐛 fix: bugs fixed
2025-07-27 00:27:20 +02:00

363 lines
11 KiB
Vue

<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 VScrollpanel from '../scrollpanel/VScrollpanel.vue';
import VLabelErrorProxy from './VLabelErrorProxy.vue';
import type IVFileUpload from './IVFileUpload.type';
import type { FileUploadErrorEvent, FileUploadProgressEvent, FileUploadRemoveEvent, FileUploadSelectEvent, FileUploadUploadEvent } from 'primevue/fileupload';
import { computed, useId, ref } from 'vue';
import styles from '@visua/typography.module.css';
const fileUploadRef = ref();
const fileProgressMap = ref<Record<string, number>>({});
const hasActiveError = ref(false);
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<IVFileUpload>(), {
id: () => useId(),
label: 'Ajouter un fichier',
validMessage: '',
hint: '',
disabled: false,
invalidFileSizeMessage: 'Taille de fichier {0} 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',
]);
defineExpose({
upload: () => fileUploadRef.value.upload()
});
const handleSelect = (event: FileUploadSelectEvent) => {
emit('select', event);
if (!props.advanced && event.files.length > 0 && fileUploadRef.value) {
fileUploadRef.value.files = [event.files.at(-1)];
fileUploadRef.value.uploadedFiles = [];
}
};
const handleClear = () => {
emit('clear');
hasActiveError.value = false;
};
const handleUpload = (event: FileUploadUploadEvent) => {
emit('upload', event);
};
const handleRemove = (event: FileUploadRemoveEvent) => {
emit('remove', event);
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;
if (!props.advanced && fileUploadRef.value) {
fileUploadRef.value.uploadedFiles = [];
}
}
const lastSelectedFile = computed(() => {
const files = fileUploadRef.value?.files || [];
return files.at(-1) ?? null;
});
const uploadEvent = (callback: () => void) => {
callback();
};
const padding = computed(() => props.advanced ? '1.125rem' : '0rem')
const borderColor = computed(() => props.advanced ? 'var(--border-default-grey)' : 'transparent');
const labelState = computed(() => {
if(!hasActiveError.value && !props.disabled) return 'default';
else if(hasActiveError.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">
<VButtonGroup
v-if="props.advanced"
: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"
/>
<div v-else class="simple">
<VButton
label="Parcourir..."
:disabled="props.disabled"
size="sm"
@click="slotProps.chooseCallback"
title="parcourir les fichiers"
/>
<span
v-if="(!slotProps.files || slotProps.files.length === 0) && (!slotProps.uploadedFiles || slotProps.uploadedFiles.length === 0) && !hasActiveError"
:class="[styles['text-body-SM-detail-text-Regular']]"
>
Aucun fichier sélectionné
</span>
</div>
</template>
<template #empty v-if="props.advanced && !hasActiveError">
<div class="fileupload-empty" :class="[styles['text-body-SM-detail-text-Regular']]">
<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>
<template #content="slotProps">
<div v-if="props.advanced && !hasActiveError" style="margin-top: 0.75rem;" class="file-content">
<VMessage
v-if="globalStatusMessage"
:type="globalStatusMessage.type"
:title="globalStatusMessage.title"
/>
<VScrollpanel height="20rem">
<div class="file-area">
<div v-if="slotProps.files.length > 0" class="file-non-uploaded-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 v-if="slotProps.uploadedFiles.length > 0" class="file-uploaded-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>
</VScrollpanel>
</div>
<div v-if="!props.advanced && !hasActiveError">
<VFile
v-if="lastSelectedFile"
:file="lastSelectedFile"
:remove-file-callback="slotProps.removeFileCallback"
status="pending"
/>
<VFile
v-if="slotProps.uploadedFiles.length > 0 && slotProps.uploadedFiles.at(-1)"
:file="slotProps.uploadedFiles.at(-1)!"
:remove-file-callback="slotProps.removeFileCallback"
status="completed"
/>
</div>
<div
role="alert"
aria-live="polite"
>
<VHint
v-for="message of slotProps.messages"
:key="message"
:title="message"
type="alert"
icon
/>
</div>
<VLabelErrorProxy :hasError="(slotProps.messages ?? []).length > 0" @update:error="hasActiveError = $event" />
</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{
display: flex;
flex-direction: row;
align-items: center;
gap: var(--p-fileupload-file-gap);
}
.fileupload-empty{
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1rem;
gap: 1.5rem;
}
.upload-cloud-icon{
font-size: 5rem;
color: var(--border-contrast-grey);
}
.file-content,
.file-area{
display: flex;
flex-direction: column;
}
.file-content{gap: 0.75rem;}
.file-area{gap: 0.5rem;}
.file-non-uploaded-area,
.file-uploaded-area {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.25rem;
}
</style>