Upgrade reusable ImageInputComponent

This commit is contained in:
Jérémy Munsch 2022-06-07 11:56:20 +02:00
parent 1d3aa36d44
commit 77b430bdac
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC

View file

@ -1,25 +1,29 @@
<template>
<div class="container ImageInput p-0">
<div class="row">
<div class="col-12 col-md-6 form-group">
<label :for="id" class="col-form-label"><b>{{ __('Choisir un fichier') }}</b></label>
<div :class="`col-12 ${_preview ? 'col-md-6' : ''} form-group`">
<label v-if="_preview" :for="_id" class="col-form-label"><b>{{ __('Choisir un fichier') }}</b></label>
<div class="input-group">
<button v-on:click.prevent="chooseAFile" class="btn btn-outline-secondary" type="button">{{ __('Parcourir') }}</button>
<input :id="id" ref="actualImage" @change="changedImage" type="file" class="d-none" :name="name" accept="image/png,image/jpeg,.svg" :aria-describedby="`${id}Help`">
<input v-on:click.prevent="chooseAFile" type="text" :value="valueFileName" class="form-control rightAligned" :aria-describedby="`${id}Help`" readonly>
<button v-if="!required" v-on:click.prevent="removeFile" class="btn btn-outline-danger" type="button"><i class="fas fa-eraser"></i></button>
<button v-on:click.prevent="prepareImageEditor" :disabled="!hasImage" class="btn btn-primary" type="button" data-bs-toggle="modal" :data-bs-target="`#${id}Modal`">
<input :id="_id" ref="actualImage" @change="changedImage" type="file" class="d-none" :name="_name" accept="image/png,image/jpeg,.svg" :aria-describedby="`Help${_id}`">
<input v-if="typeof _value === 'string'" class="d-none" :name="_name" type="text" :value="_value">
<input v-on:click.prevent="chooseAFile" type="text" :value="_valueFileName" class="form-control rightAligned" :aria-describedby="`Help${_id}`" readonly>
<button v-if="!_required" v-on:click.prevent="removeFile" class="btn btn-outline-danger" type="button"><i class="fas fa-eraser"></i></button>
<button v-if="!_preview && _value" class="btn btn-warning" type="button" data-bs-toggle="modal" :data-bs-target="`#ModalPreview${_id}`">
<span data-bs-toggle="tooltip" :title="__('Visualiser l\'image')"><i class="fa-solid fa-eye"></i></span>
</button>
<button v-on:click.prevent="prepareImageEditor($event, _id)" :disabled="!_hasImage" class="btn btn-primary" type="button" data-bs-toggle="modal" :data-bs-target="`#Modal${_id}`">
<span data-bs-toggle="tooltip" :title="__('Redimensionner l\'image')"><i class="fa-solid fa-crop"></i></span>
</button>
</div>
<small :id="`${id}Help`" class="form-text text-muted">{{ helper }}</small>
<small :id="`Help${_id}`" class="form-text text-muted">{{ _helper }}</small>
</div>
<div class="col-12 col-md-6 form-group">
<small v-if="value" :id="`${id}PreviewHelp`" class="form-text text-muted d-block">{{ __('Prévisualisation de l\'image') }}</small>
<img ref="preview" v-if="value" class="img-fluid" :src="value" :alt="__('Image placeholder')" :aria-describedby="`${id}PreviewHelp`">
<div v-if="_preview" :class="`col-12 ${_preview ? 'col-md-6' : ''} form-group`">
<small v-if="_value" :id="`PreviewHelp${_id}`" class="form-text text-muted d-block">{{ __('Prévisualisation de l\'image') }}</small>
<img v-if="_value" class="img-fluid" :src="_value" :alt="__('Image placeholder')" :aria-describedby="`PreviewHelp${_id}`">
</div>
</div>
<div :id="`${id}Modal`" class="modal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false">
<div :id="`Modal${_id}`" class="modal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
@ -82,8 +86,8 @@
</div>
</div>
<div class="mt-1 ms-4 rotateDrag">
<label for="range" class="form-label me-4">{{ __(':deg degrès', { deg: rotationText }) }} </label>
<input ref="rotationSlider" @input="rotationChange($event)" @change="rotateTo($event.target.value)" type="range" class="form-range" id="range" min="0" max="360" value="0">
<label for="range" class="form-label me-4">{{ __(':deg degrès', { deg: _rotationText }) }} </label>
<input ref="rotationSlider" v-on:input.stop="rotationChange($event)" v-on:change.stop="rotateTo($event.target.value)" type="range" class="form-range" id="range" min="0" max="360" value="0">
</div>
</div>
</div>
@ -93,7 +97,7 @@
<img class="imageModification" ref="imageModification" src="">
</div>
<div :class="`col-12 ${displaySideBySide ? 'col-md-6' : ''} p-0 bg-light previewIMG`">
<div :id="`${id}Preview`" :style="previewStyle"></div>
<div :id="`Preview${_id}`" :style="previewStyle"></div>
</div>
</div>
</div>
@ -105,38 +109,146 @@
</div>
</div>
</div>
<div v-if="!_preview && _value" :id="`ModalPreview${_id}`" class="modal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ __('Prévisualisation') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="__('Fermer la fenêtre')" data-bs-toggle="tooltip" :title="__('Fermer la fenêtre sans valider le contenu')"></button>
</div>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-12 text-center">
<small :id="`PreviewHelp${_id}`" class="form-text text-muted d-block">{{ __('Prévisualisation de l\'image') }}</small>
<img class="img-fluid" :src="_value" :alt="__('Image placeholder')" :aria-describedby="`PreviewHelp${_id}`">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Cropper from "cropperjs/dist/cropper.js"
export default {
emits: ['browseFile'],
mounted() {
// * INIT Bootstrap tooltips
let tooltipTriggerList = [].slice.call(document.querySelectorAll('.ImageInput [data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, { delay: { show: 1500, hide: 300 } })
})
let data = JSON.parse(this.$attrs.json)
this.id = data.id
this.name = data.name
this.helper = data.helper ?? ''
this.width = data.width
this.height = data.height
this.value = String(data.value)
if(this.value.length) {
this.valueFileName = this.value
this.value = this.value.replace(/^\/|^/, '/')
if(this.$attrs.json) {
// * Init uppon Laravel Blade
let data = JSON.parse(this.$attrs.json)
this._id = data.id
this._name = data.name
this._helper = data.helper ?? ''
this._width = data.width
this._height = data.height
this._value = String(data.value)
if(this._value.length) {
this._valueFileName = this._value
this._value = this._value.replace(/^\/|^/, '/')
}
if(data.preview !== undefined) {
this._preview = data.preview ? true : false
}
if(data.required !== undefined) {
this._required = data.required ? true : false
}
if(data.browseEvent !== undefined) {
this._browseEvent = data.browseEvent ? true : false
}
} else {
// * Init Uppon Vue component
this._id = this.id
this._name = this.name
this._helper = this.helper
this._width = this.width
this._height = this.height
this._value = this.value
if(this._value.length) {
this._valueFileName = this._value
this._value = this._value.replace(/^\/|^/, '/')
}
this._preview = this.preview
this._required = this.required
this._browseEvent = this.browseEvent
}
if(data.required !== undefined) {
this.required = data.required ? true : false
this._hasImage = false
},
props: {
id: {
default: ''
},
required: {
type: Boolean,
default: false
},
name: {
type: String,
default: ''
},
helper: {
type: String,
default: ''
},
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
value: {
type: String,
default: ''
},
preview: {
type: Boolean,
default: true
},
browseEvent: {
type: Boolean,
default: false
}
},
data() {
return {
_id: '',
_value: '',
_valueFileName: this.__('Sélectionnez une image'),
_helper: '',
_name: '',
_required: true,
_width: 0,
_height: 0,
_preview: true,
_browseEvent: false,
_rotationText: 0,
_rotationValue: 0,
_scaleX: 1,
_scaleY: 1,
_hasImage: false,
_cropper: null,
__originalFile : null,
}
},
computed: {
previewStyle() {
return `width: ${(this.width / 1.5).toFixed(3)}px;height:${(this.height / 1.5).toFixed(3)}px;${!this.displaySideBySide ? 'position: relative;' : ''}`
return `width: ${(this._width / 1.5).toFixed(3)}px;height:${(this._height / 1.5).toFixed(3)}px;${!this.displaySideBySide ? 'position: relative;' : ''}`
},
imageRatio() {
return this.width / this.height
return this._width / this._height
},
displaySideBySide() {
return this.imageRatio <= 1
@ -158,103 +270,113 @@ export default {
cropBoxMovable: true,
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
minContainerHeight: (this.height * 1.1).toFixed(2),
minCropBoxWidth: this.width,
minCropBoxHeight: this.height,
aspectRatio: this.width / this.height,
preview: `#${this.id}Preview`
minContainerHeight: (this._height * 1.1).toFixed(2),
minCropBoxWidth: this._width,
minCropBoxHeight: this._height,
aspectRatio: this._width / this._height,
preview: `#Preview${this._id}`
}
}
},
data() {
return {
id: '',
required: true,
name: '',
value: '',
valueFileName: this.__('Sélectionnez une image'),
helper: '',
width: 0,
height: 0,
rotationText: 0,
rotationValue: 0,
scaleX: 1,
scaleY: 1,
hasImage: false,
cropper: null
}
},
watch: {
rotationValue(value) {
this.cropper.rotateTo(value)
_rotationValue(value) {
this._cropper.rotateTo(value)
},
_value() {
if (this._browseEvent) {
this._hasImage = true
}
},
value() {
this._value = this.value.replace(/^\/|^/, '/')
this._valueFileName = this.value
}
},
methods: {
reset() {
this.cropper.reset()
this._cropper.reset()
},
zoom(v) {
this.cropper.zoom(v)
this._cropper.zoom(v)
},
move(x, y) {
this.cropper.move(x, y)
this._cropper.move(x, y)
},
scale(horizontal) {
if(horizontal) {
this.scaleX = -1/this.scaleX
this.cropper.scaleX(this.scaleX)
this._scaleX = -1/this._scaleX
this._cropper.scaleX(this._scaleX)
} else {
this.scaleY = -1/this.scaleY
this.cropper.scaleY(this.scaleY)
this._scaleY = -1/this._scaleY
this._cropper.scaleY(this._scaleY)
}
},
rotate(value) {
let res = Number.parseInt(this.rotationValue) + Number.parseInt(value)
let res = Number.parseInt(this._rotationValue) + Number.parseInt(value)
res = res < 0 ? 0 : res
res = res > 360 ? 360 : res
this.rotationValue = res
this.rotationText = this.rotationValue
this.$refs.rotationSlider.value = this.rotationValue
this._rotationValue = res
this._rotationText = this._rotationValue
this.$refs.rotationSlider.value = this._rotationValue
},
rotateTo(value) {
value = value < 0 ? 0 : value
value = value > 360 ? 360 : value
this.rotationValue = Number.parseInt(value)
this.rotationText = this.rotationValue
this._rotationValue = Number.parseInt(value)
this._rotationText = this._rotationValue
},
rotationChange(event) {
const value = event.target.value
this.rotateTo(value)
},
chooseAFile(e) {
if(this._browseEvent) {
this.$emit('browseFile', this._id)
return;
}
this.$refs.actualImage.click()
},
removeFile(e) {
this.$refs.actualImage.value = ''
this.valueFileName = this.__('Sélectionnez une image')
this.value = ''
this._valueFileName = this.__('Sélectionnez une image')
this._value = ''
},
changedImage(e) {
const self = this,
reader = new FileReader()
self.hasImage = true
self._hasImage = true
reader.addEventListener('load', () => {
// On convertit l'image en une chaîne de caractères base64.
self.value = reader.result
self._value = reader.result
}, false)
if (e.target.files[0]) {
reader.readAsDataURL(e.target.files[0])
this.valueFileName = e.target.files[0].name
this._valueFileName = e.target.files[0].name
}
},
prepareImageEditor() {
const image = this.$refs.actualImage.files[0]
this.$refs.imageModification.src = URL.createObjectURL(image)
if(this.cropper) {
this.cropper.destroy()
prepareImageEditor(e, _id) {
const self = this,
createCropper = () => {
if(self._cropper) {
self._cropper.destroy()
}
self._cropper = new Cropper(self.$refs.imageModification, self.cropperOption)
}
if (!self.browseEvent) {
self.__originalFileName = self.$refs.actualImage.files[0].name
self.$refs.imageModification.src = URL.createObjectURL(self.__originalFile)
createCropper()
return;
}
this.cropper = new Cropper(this.$refs.imageModification, this.cropperOption)
e.preventDefault()
fetch(self.value.replace(/^\/|^/, '/')).then((response) => {
self.__originalFileName = response.url.substring(response.url.lastIndexOf('/')+1)
return response.blob()
}).then(blob => {
self.$refs.imageModification.src = URL.createObjectURL(blob)
createCropper()
})
},
dataURLtoBlob(dataurl) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
@ -266,18 +388,18 @@ export default {
},
exportToBlob() {
const self = this
const originalFile = self.$refs.actualImage.files[0]
const container = new DataTransfer()
this.value = this.cropper.getCroppedCanvas({
width: this.width,
height: this.height,
self._value = self._cropper.getCroppedCanvas({
width: self._width,
height: self._height,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
}).toDataURL('image/png')
const blob = this.dataURLtoBlob(this.value)
const file = new File([blob], originalFile.name, {type: 'image/png', lastModified: new Date() })
const file = new File(
[self.dataURLtoBlob(self._value)],
self.__originalFileName, {type: 'image/png', lastModified: new Date() })
container.items.add(file)
this.$refs.actualImage.files = container.files
self.$refs.actualImage.files = container.files
}
}
}