171 lines
5.1 KiB
Vue
171 lines
5.1 KiB
Vue
<script lang="ts" setup>
|
|
interface Props {
|
|
modelValue?: File[]
|
|
maxFiles?: number
|
|
acceptedTypes?: string
|
|
maxFileSize?: number // in MB
|
|
disabled?: boolean
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', files: File[]): void
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelValue: () => [],
|
|
maxFiles: 5,
|
|
acceptedTypes: 'image/*,video/*',
|
|
maxFileSize: 50,
|
|
disabled: false
|
|
})
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
const fileInputRef = ref<HTMLInputElement>()
|
|
const isDragging = ref(false)
|
|
const error = ref('')
|
|
|
|
const files = computed({
|
|
get: () => props.modelValue || [],
|
|
set: (value) => emit('update:modelValue', value)
|
|
})
|
|
|
|
const handleFilesSelected = (selectedFiles: FileList | null) => {
|
|
if (!selectedFiles) return
|
|
error.value = ''
|
|
|
|
const newFiles = Array.from(selectedFiles)
|
|
|
|
if (files.value.length + newFiles.length > props.maxFiles) {
|
|
error.value = `Massimo ${props.maxFiles} file consentiti`
|
|
return
|
|
}
|
|
|
|
for (const file of newFiles) {
|
|
if (file.size > props.maxFileSize! * 1024 * 1024) {
|
|
error.value = `Il file ${file.name} supera il limite di ${props.maxFileSize}MB`
|
|
return
|
|
}
|
|
}
|
|
|
|
files.value = [...files.value, ...newFiles]
|
|
}
|
|
|
|
const removeFile = (index: number) => {
|
|
files.value.splice(index, 1)
|
|
}
|
|
|
|
const openFileDialog = () => {
|
|
fileInputRef.value?.click()
|
|
}
|
|
|
|
const formatFileSize = (bytes: number): string => {
|
|
const mb = Math.round(bytes / (1024 * 1024) * 10) / 10
|
|
return mb > 0 ? `${mb}MB` : 'Piccolo'
|
|
}
|
|
|
|
const getFileUrl = (file: File) => {
|
|
return URL.createObjectURL(file)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="media-uploader">
|
|
<div
|
|
:class="{ 'border-primary bg-primary bg-opacity-10': isDragging, 'bg-light': !isDragging }"
|
|
class="upload-area border-2 border-dashed rounded-3 p-4 text-center cursor-pointer position-relative"
|
|
@click="!disabled && openFileDialog()"
|
|
@dragover.prevent="isDragging = true"
|
|
@dragleave.prevent="isDragging = false"
|
|
@drop.prevent="isDragging = false; handleFilesSelected(($event as DragEvent).dataTransfer?.files || null)"
|
|
>
|
|
<BIIcon class="mb-3" color="primary" name="it-upload" size="xl"/>
|
|
<div class="h6 font-weight-bold">
|
|
Trascina i file qui o <span class="text-primary text-decoration-underline">clicca per selezionare</span>
|
|
</div>
|
|
<small class="text-muted d-block mt-2">
|
|
Massimo {{ maxFiles }} file ({{ maxFileSize }}MB max per file). Tipi: {{ acceptedTypes }}
|
|
</small>
|
|
</div>
|
|
|
|
<input
|
|
ref="fileInputRef"
|
|
:accept="acceptedTypes"
|
|
:disabled="disabled"
|
|
:multiple="maxFiles > 1"
|
|
class="d-none"
|
|
type="file"
|
|
@change="handleFilesSelected(($event.target as HTMLInputElement).files)"
|
|
/>
|
|
|
|
<div v-if="error" class="alert alert-danger mt-3 py-2 small" role="alert">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<div v-if="files.length > 0" class="mt-4 row g-3">
|
|
<div v-for="(file, idx) in files" :key="`file-${idx}`" class="col-6 col-md-4 col-lg-3">
|
|
<div class="card h-100 position-relative shadow-sm border-0">
|
|
<div class="ratio ratio-1x1 bg-light rounded-top overflow-hidden">
|
|
<img v-if="file.type.startsWith('image/')" :alt="file.name" :src="getFileUrl(file)"
|
|
class="object-fit-cover"/>
|
|
<video v-else-if="file.type.startsWith('video/')" :src="getFileUrl(file)" class="object-fit-cover"></video>
|
|
<div v-else class="d-flex align-items-center justify-content-center bg-secondary bg-opacity-25">
|
|
<BIIcon color="secondary" name="it-file" size="xl"/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
:disabled="disabled"
|
|
class="btn btn-danger btn-sm position-absolute top-0 end-0 m-1 rounded-circle p-1 d-flex align-items-center justify-content-center shadow"
|
|
style="width: 28px; height: 28px; z-index: 10;"
|
|
title="Rimuovi file"
|
|
type="button"
|
|
@click.stop="removeFile(idx)"
|
|
>
|
|
<BIIcon color="white" name="it-close" size="sm"/>
|
|
</button>
|
|
|
|
<div class="card-body p-2 d-flex flex-column justify-content-between">
|
|
<div :title="file.name" class="small text-truncate fw-semibold">{{ file.name }}</div>
|
|
<div class="small text-muted d-flex justify-content-between align-items-center mt-1">
|
|
<span>{{ formatFileSize(file.size) }}</span>
|
|
<div class="progress flex-grow-1 ms-2" style="height: 4px;" title="Pronto per l'upload">
|
|
<div class="progress-bar bg-success w-100" role="progressbar"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.upload-area {
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
|
|
.upload-area:hover:not(.border-primary) {
|
|
background-color: #f8f9fa !important;
|
|
border-color: #0066cc !important;
|
|
}
|
|
|
|
.cursor-pointer {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.border-dashed {
|
|
border-style: dashed !important;
|
|
}
|
|
|
|
.border-2 {
|
|
border-width: 2px !important;
|
|
}
|
|
|
|
.object-fit-cover {
|
|
object-fit: cover;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|