Upgrade reusable ImageInputComponent
This commit is contained in:
parent
1d3aa36d44
commit
77b430bdac
1 changed files with 207 additions and 85 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue