# Componente
# Introducción
Este componente renderizará un input de tipo file picker. El mismo se compone de dos partes, una de renderizado y otra de lógica (IrFilePicker Bone), esta ultima será la encargada de el manejo de los datos.
# Uso
Es importante recalcar el nombre del componente IrFilePicker a nivel de código, ya que este deberá usarse para llamar al componente para renderizarlo, ya sea individualmente o en un IrForm.
# Ejemplo
El ejemplo no funcionará correctamente debido a que no se pueden subir archivos al servidor BE desde la documentación
# Propiedades
| Propiedad | Tipo | Default | Descripción |
| readonly | Boolean |
| El valor |
| value | Array | Array de filenames. | |
| required | Boolean |
| En valor |
| placeholder | String |
| Establece el placeholder en el campo. |
| accept | String |
| Un String que defina los tipos de archivos que se podrán seleccionar en el file picker. La documentación de los valores de access se encuentran aquí |
| imageCompressOptions | Object |
| Objeto que define opciones de compresión de imágenes previa a la subida al servidor. Solo aplicará si se encuentra definida y si el archivo a subir es una imagen. El detalle de opciones se encuentra aquí |
| subfolder | String |
| DEPRECADA por propiedad folderDefinen un subdirectorio donde se guardará el archivo subido. |
| folder | String |
| Define un directorio donde se guardará el archivo subido. Si no existe la carpeta se creará solo la de ultimo nivel . Es decir si folder: /var/www/upload y upload no existe se creará. Pero si no existe www no. Ya que solo crea si la ultima carpeta no existe. Esta carpeta siempre será relativa a /mnt/volume_nyc1_01_ir/ y no se podrá navegar hacia arriba en las carpetas, es decir, está anulado el uso de ../ |
| url | String |
| Definen la URL que se utilizará para formar los preview concatenado con el value del archivo. |
| preview | Boolean |
| |
| multiple | Boolean |
| Permite subir varios archivos a la vez |
| save-draft | Object | undefined | Establece una función y argumentos de la misma. La cual se usará para hacer un submit del formulario en el estado que se encuentre cuando se suba el archivo al servidor. A la función se enviará la misma estructura que al comportamiento fcn executer de formulario que puede verse aquí Esta propiedad espera una estructura definida de la siguiente manera: |
| hint | Object | String |
| Genera un texto de ayuda para el componente, que podrá ser o no, clickeable para navegar a un link determinado. Propiedad con estructura de objeto con los siguientes keys: o simplemente un String. En la propiedad En la propiedad En la propiedad En la propiedad Las clases aplicables para las filas de la tabla podrán encontrarse en Vuetify en los siguientes enlaces:
|
| linked | Boolean | false | Determina la actualización del formulario contenedor. |
| base64 | Array | undefined | Si está definida como array vacío, se generará el base64 de los archivos subidos por el usuario y se corresponderán en el orden del arreglo con la propiedad |
| link | Array | null | Si está definida como array (incluso vacío), luego de cada subida exitosa de archivos valorizará un array con las URLs completas de los archivos subidos. Estas URLs se construyen concatenando la prop A diferencia de |
# Código fuente
<template>
<ir-file-picker-bone
v-bind="$attrs"
v-model="localValue"
:file="file"
@input="changeValue"
@input-base64="changebase64"
@update:link="updateLinkValue"
>
<div
slot-scope="{
imgEvents,
fileName,
areAllImages,
fileSetted,
loading,
inputEvents,
required,
removeEvents,
upload
}"
>
<ir-image-gallery
v-if="fileName && fileSetted && preview && areAllImages && !loading"
:images="getFilesLinks(fileName)"
:img-events="imgEvents"
/>
<div
v-if="fileSetted && preview && !loading && url && !areAllImages"
style="position: relative"
>
<template v-if="!localUrl">
<a
v-for="file in fileName"
:key="file"
:href="`${url}${file}`"
target="_blank"
>
<v-btn
class="mr-2 mb-2"
outlined
rounded
color="primary"
download
>
{{ filenameShort(file.split('/').pop()) }}
</v-btn>
</a>
</template>
<template v-else>
<a
v-for="fileIterado in fileArray"
:key="fileIterado.name"
:href="getFileLink(fileIterado)"
target="_blank"
>
<v-btn
class="mr-2 mb-2"
outlined
rounded
color="primary"
download
>
{{ filenameShort(fileIterado.name.split('/').pop()) }}
</v-btn>
</a>
</template>
</div>
<div class="d-flex">
<v-file-input
:ref="irFilePickerRef"
v-model="file.file"
data-cy="filepicker"
prepend-icon="mdi-paperclip"
:loading="loading"
:clearable="false"
v-bind="getFilteredProps"
:rules="
required == true
? [
(v) =>
(v && (v.length > 0 || fileName != '')) ||
'Complete este campo'
]
: []
"
:hide-details="false"
v-on="inputEvents"
@change="(e) => setLocalPreview(e, upload)"
>
<template #message="{ message }">
<div v-html="message" />
</template>
</v-file-input>
<div
class="d-flex tw-align-center tw-cursor-pointer"
v-on="removeEvents"
>
<v-icon>mdi-delete</v-icon>
</div>
</div>
</div>
</ir-file-picker-bone>
</template>
<script>
import IrFilePickerBone from './IrFilePickerBone.vue'
import IrImageGallery from '../../IrImageGallery/IrImageGallery.vue'
import { filePickerComponentName } from '../../../constants'
import { setHintTag } from '../../../helpers/hint'
import { getBaseName } from '../../../helpers/getFilesBaseNames'
import focusAndScrollMixin from '../../../mixins/focusAndScroll'
import { filterProps } from '../../../helpers/filterProps'
export default {
name: filePickerComponentName,
components: {
IrFilePickerBone,
IrImageGallery
},
mixins: [focusAndScrollMixin],
inheritAttrs: false,
props: {
preview: {
type: Boolean,
default: false
},
value: { type: Array, required: true },
url: { type: String, required: false, default: undefined },
saveDraft: { type: Object, required: false, default: undefined },
linked: { type: Boolean, required: false, default: false },
isFocused: {
type: Boolean,
default: false
},
hint: {
type: [String, Object],
default: ''
},
link: { type: Array, required: false, default: null }
},
data () {
return {
irFilePickerRef: filePickerComponentName,
file: { file: [] },
localValue: this.value,
otherProps: { ...this.$attrs },
localUrl: false,
currentFiles: null
}
},
computed: {
getFilteredProps () {
return filterProps(this.$refs[this.irFilePickerRef], this.handledAttrs, [
'data-cy'
])
},
fileArray () {
//Retorna siempre un arreglo de archivos para poder iterarlo
if (Array.isArray(this.file.file)) return this.file.file
else return [this.file.file]
},
handledAttrs () {
// Elimina attrs que son válidos para el componente de vuetify, pero que son manejados por el componente IR
const hint = setHintTag(this.otherProps, this.hint) //from hintMaker mixin
let aux = { ...this.otherProps, ...this.$attrs }
delete aux.hint
if (hint) {
aux = { ...aux, hint }
}
delete aux.value
delete aux.required
delete aux.readonly
delete aux.preview
delete aux.isImage
// delete aux.hint in hintMaker mixin
return aux
}
},
watch: {
value (isValue) {
//convierte value para v-model de v-file-input de tipo File
this.localValue = isValue
let blobPreload = []
if (Array.isArray(isValue)) {
isValue.forEach((file) => {
let blob = new File([], file)
blobPreload.push(blob)
})
}
this.file.file = isValue ? blobPreload : null
}
},
mounted () {
if (this.isFocused) {
let element = this.$refs[this.irFilePickerRef].$el.querySelector('input')
this.focusAndScroll(element)
}
this.localValue = this.value
//Inicializacion de value para file input de tipo File
let blobPreload = []
if (Array.isArray(this.value)) {
this.value.forEach((file) => {
let blob = new File([], file)
blobPreload.push(blob)
})
}
this.file.file = this.value ? blobPreload : null
this.otherProps = this.$attrs // Se asigna en mounted para inicializar que funcione hintMaker y volver a calcular handledAttrs
},
methods: {
getFilesLinks (fileName) {
//Genera los links de los archivos dependiendo si son preview locales o value obtenido de BD
if (this.localUrl) {
return [this.localUrl]
} else {
return fileName.map((file) => `${this.url}${file}`)
}
},
getFileLink (file) {
//Retorna la url local de un archivo
return URL.createObjectURL(file)
},
setLocalPreview (archivos, upload) {
// Normalizar ambos a arrays
const currentFilesArray = Array.isArray(this.currentFiles)
? this.currentFiles
: [this.currentFiles]
const archivosArray = Array.isArray(archivos) ? archivos : [archivos]
// Comparar si hay alguna diferencia en los nombres de archivo
const areDifferent =
archivosArray.length !== currentFilesArray.length ||
archivosArray.some(
(file, index) =>
getBaseName(file) !== getBaseName(currentFilesArray[index])
)
// Si todos los archivos son iguales, no continuar
if (!areDifferent) return
this.currentFiles = archivosArray
//determina la URL par los archivos locales, tanto para uno o multiples
if (!archivos) return
if (Array.isArray(archivos))
this.localUrl = archivos.map((file) => URL.createObjectURL(file))
else this.localUrl = URL.createObjectURL(archivos)
upload()
},
changeValue (v) {
if (Array.isArray(v) && v.length > 0) {
this.$emit('input', v)
if (this.linked === true) this.$emit('change-linked-value')
if (typeof this.saveDraft !== 'undefined')
this.$emit('saveDraft', this.saveDraft)
} else {
//on clear input se emite value array vacío
this.file.file = null
this.$emit('input', [])
}
},
changebase64 (value) {
this.$emit('update-prop', { prop: 'base64', value })
},
updateLinkValue (value) {
if (Array.isArray(this.link)) {
if (Array.isArray(value)) {
const newLinks = value.map((file) => `${this.url}${file}`)
this.$emit('update-prop', { prop: 'link', value: newLinks })
} else {
this.$emit('update-prop', { prop: 'link', value: [`${this.url}${value}`] })
}
}
},
filenameShort (filename) {
//Acorta los nombre de los archivos
let ext = filename.split('.').pop()
if (filename.length > 10) {
return filename.slice(0, 20) + '...' + ext
}
}
}
}
</script>
# Código fuente
<script>
const props = {
value: { type: Array, required: true },
file: { type: Object, required: true },
base64: { type: Array },
required: { type: Boolean, default: false },
subfolder: { type: String }, // sub carpeta donde se guardará el archivo subido
folder: { type: String }, // mismo que subfolder, mejora del nombre, se mantienen ambas para retrocompatibilidad
imageCompressOptions: { type: Object }, // Activa la compresión de imágenes a la subida
}
import {
normalizeProps,
assignCamelToSnake
} from '../../../helpers/propsGenerator'
import { endpointIclUpload } from '../../../../src/constants'
import imageCompression from 'browser-image-compression'
const mergedProps = normalizeProps(props)
import { filePickerComponentName } from '../../../constants'
export default {
name: filePickerComponentName + 'Bone',
props: mergedProps,
data () {
return {
loading: false,
fileSelected: 0,
fileName: this.file,
structureError: false,
isUpdatingByUpload: false
}
},
computed: {
areAllImages () {
//determina si todos los archivos son imágenes, para mostrar preview de galería
if (!Array.isArray(this.fileName)) return false
for (let file of this.fileName) {
if (
!new RegExp(
/\.(jpg|jpeg|png|gif|webp|bmp|svg|ico|tiff|tif|jfif|pjpeg|pjp|avif|apng|svgz|jpe|xbm)$/i
).test(file)
) {
return false
}
}
return true
},
fileSetted () {
//Si this.file.file es array con elementos o si es object pero no es array ni null, entonces se muestra la foto
return (
(Array.isArray(this.file.file) && this.file.file.length > 0) ||
(typeof this.file.file === 'object' &&
!Array.isArray(this.file.file) &&
this.file.file !== null)
)
},
checkRequired () {
if (this.required === true) {
return this.file.length <= 0
} else {
return false
}
}
},
watch: {
file () {
this.uploadFile()
}
},
created () {
assignCamelToSnake(mergedProps, this)
},
mounted () {
this.fileName = this.value
this.structureError = this.$propError(
props,
this.$props,
filePickerComponentName
)
if (this.structureError) {
this.$iclstore.commit('updateSnack', {
text: 'Ha ocurrido un error al cargar algunos campos del formulario',
active: true,
color: 'warning'
})
}
},
methods: {
/**
* genera y retorna el string base64 de un archivo.
* @async
* @function getBase64
* @param {File} file - Archivo a convertir en base64
* @returns {Promise<string>} String base64 del archivo pasado como parámetro.
*/
getBase64 (file) {
return new Promise((resolve, reject) => {
var reader = new FileReader()
reader.onload = () => {
let base64Str = reader.result
if (base64Str) return resolve(base64Str)
return reject(new Error('Se produjo un error generando base64'))
}
reader.onerror = () => {
reject(new Error('Error al generar base64'))
}
reader.readAsDataURL(file)
})
},
/**
* Sube un archivo o varios archivos al servidor mediante una solicitud POST usando FormData. Emitiendo el evento input con el valor retornado por el BE.
* y genera el base64 del archivo si está definida la prop base64 como array
* @async
* @function uploadFile
* @returns {Promise<boolean>} True si el archivo se subió correctamente, de lo contrario, devuelve false.
*/
async uploadFile () {
if (this.isUpdatingByUpload) return
this.isUpdatingByUpload = true
this.loading = true
let isGenerateBase64 = Array.isArray(this.base64)
let base64Aux = []
try {
if (!this.file || !this.file.file || this.file.file.length === 0) {
return true
}
const form = new FormData()
if (this.subfolder) form.append('subfolder', this.subfolder)
else if (this.folder) form.append('subfolder', this.folder)
if (Array.isArray(this.file.file)) {
for (let file of this.file.file) {
// Appendea al form cada file, si se envía directamente el file da error en API y si existe la prop base64 lo genera
const isImage = file?.type?.includes('image')
const blobOfFile =
isImage && this.imageCompressOptions
? await imageCompression(file, this.imageCompressOptions)
: file
const fileToUpload =
isImage && this.imageCompressOptions
? new File([blobOfFile], file.name)
: file
form.append('files', fileToUpload)
if (isGenerateBase64) {
let base64 = await this.getBase64(fileToUpload) //hecho en uploadFile para iterar una unica vez en el arreglo
base64Aux.push(base64)
}
}
} else {
const isImage = this.file?.file?.type?.includes('image')
const blobOfFile =
isImage && this.imageCompressOptions
? await imageCompression(
this.file.file,
this.imageCompressOptions
)
: this.file.file
const fileToUpload =
isImage && this.imageCompressOptions
? new File([blobOfFile], this.file.file.name)
: this.file.file
form.append('files', fileToUpload)
if (isGenerateBase64) {
let base64 = await this.getBase64(fileToUpload)
base64Aux.push(base64)
}
}
const upload = await this.$iclAxios.post(endpointIclUpload, form, {
headers: { 'Content-Type': 'multipart/form-data' }
})
this.fileName = upload.data.filename
if (isGenerateBase64) this.$emit('input-base64', base64Aux)
this.$emit('input', upload.data.filename)
this.$emit('update:link', upload.data.filename)
if (upload) return true
else return false
} catch (e) {
//now error in $uploadFile
console.log(e)
if (isGenerateBase64) this.$emit('input-base64', [])
this.$emit('input', [])
return false
} finally {
this.loading = false
}
},
elimina () {
this.file.file = null
this.fileName = []
this.$emit('input', this.fileName)
},
closeDialog () {
this.file.dialogImg = false
this.fileSelected = 0
},
nextFile () {
this.fileSelected++
},
prevFile () {
this.fileSelected--
}
},
render () {
if (this.structureError) {
return
}
return this.$scopedSlots.default({
inputEvents: {
focusout: (e) => {
this.isUpdatingByUpload = false
}
},
removeEvents: {
click: this.elimina
},
upload: this.uploadFile,
fileName: this.fileName,
fileSetted: this.fileSetted,
file: this.file.file,
loading: this.loading,
checkRequired: this.checkRequired,
required: this.required,
areAllImages: this.areAllImages,
fileSelected: this.fileSelected,
nextFile: this.nextFile,
prevFile: this.prevFile
})
}
}
</script>