ldc-loue/resources/js/components/bo/ImageInputComponent.vue
2022-06-09 09:59:06 +02:00

468 lines
20 KiB
Vue

<template>
<div class="container ImageInput p-0">
<div class="row">
<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="`Help${_id}`">
<input v-if="_pushString && 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="`Help${_id}`" class="form-text text-muted">{{ _helper }}</small>
</div>
<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="`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">
<h5 class="modal-title">{{ __('Modification d\'image avant import') }}</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 text-center">
<div class="btn-group me-4 mt-2">
<button class="btn btn-warning" data-bs-toggle="tooltip" :title="__('Réinitialiser l\'image')" v-on:click.prevent="reset()">
<i class="fa fa-sync-alt"></i>
</button>
</div>
<div class="btn-group me-4 mt-2">
<button class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Zoom avant')" v-on:click.prevent="zoom(0.1)">
<i class="fa fa-search-plus"></i>
</button>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Zoom arrière')" v-on:click.prevent="zoom(-0.1)">
<i class="fa fa-search-minus"></i>
</a>
</div>
<div class="btn-group me-4 mt-2">
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers la gauche')" v-on:click.prevent="move(-10, 0)">
<i class="fa fa-arrow-left"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers la droite')" v-on:click.prevent="move(10, 0)">
<i class="fa fa-arrow-right"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers le haut')" v-on:click.prevent="move(0, -10)">
<i class="fa fa-arrow-up"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers le bas')" v-on:click.prevent="move(0, 10)">
<i class="fa fa-arrow-down"></i>
</a>
</div>
<div class="btn-group me-4 mt-2">
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Mirroir Horizontal')" v-on:click.prevent="scale(true)">
<i class="fa fa-arrows-alt-h"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Mirroir Vertical')" v-on:click.prevent="scale(false)">
<i class="fa fa-arrows-alt-v"></i>
</a>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex flex-row">
<div class="rotateButton">
<span class="text-dark">{{ __('Rotation') }}&nbsp;:</span>
<div class="btn-group mt-1">
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Rotation sens anti-horaire')" v-on:click.prevent="rotate(-45)">
<i class="fa fa-undo-alt"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Rotation sens horaire')" v-on:click.prevent="rotate(45)">
<i class="fa fa-redo-alt"></i>
</a>
</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" 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>
</div>
<div class="row mt-3">
<div :class="`col-12 ${displaySideBySide ? 'col-md-6' : ''} p-0 mb-3`">
<img class="imageModification" ref="imageModification" src="">
</div>
<div :class="`col-12 ${displaySideBySide ? 'col-md-6' : ''} p-0 bg-light previewIMG`">
<div :id="`Preview${_id}`" :style="previewStyle"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" :aria-label="__('Fermer la fenêtre sans valider le contenu')" data-bs-toggle="tooltip" :title="__('Fermer la fenêtre sans valider le contenu')">{{ __('Fermer') }}</button>
<button v-on:click.prevent="exportToBlob" type="button" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-dismiss="modal" :title="__('Valider le contenu et fermer la fenêtre')">{{ __('Valider') }}</button>
</div>
</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 } })
})
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
}
if(data.pushString !== undefined) {
this._pushString = data.pushString ? 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
this._pushString = this.pushString
}
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
},
pushString: {
type: Boolean,
default: true
}
},
data() {
return {
_id: '',
_value: '',
_valueFileName: this.__('Sélectionnez une image'),
_helper: '',
_name: '',
_required: true,
_width: 0,
_height: 0,
_preview: true,
_browseEvent: false,
_pushString: 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;' : ''}`
},
imageRatio() {
return this._width / this._height
},
displaySideBySide() {
return this.imageRatio <= 1
},
cropperOption() {
return {
autoCrop : true,
background: true,
autoCropArea: 1,
fillColor: 'rgba(0, 0, 0, 0)',
viewMode: '0', // full crop box
dragMode: 'move',
movable: true,
rotatable: true,
scalable: true,
zoomable: true,
zoomOnTouch: false,
zoomOnWheel: false,
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: `#Preview${this._id}`
}
}
},
watch: {
_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()
},
zoom(v) {
this._cropper.zoom(v)
},
move(x, y) {
this._cropper.move(x, y)
},
scale(horizontal) {
if(horizontal) {
this._scaleX = -1/this._scaleX
this._cropper.scaleX(this._scaleX)
} else {
this._scaleY = -1/this._scaleY
this._cropper.scaleY(this._scaleY)
}
},
rotate(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
},
rotateTo(value) {
value = value < 0 ? 0 : value
value = value > 360 ? 360 : value
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 = ''
},
changedImage(e) {
const self = this,
reader = new FileReader()
self._hasImage = true
reader.addEventListener('load', () => {
// On convertit l'image en une chaîne de caractères base64.
self._value = reader.result
}, false)
if (e.target.files[0]) {
reader.readAsDataURL(e.target.files[0])
this._valueFileName = e.target.files[0].name
}
},
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;
}
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],
bstr = window.atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
while(n--){
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], {type:mime})
},
exportToBlob() {
const self = this
const container = new DataTransfer()
self._value = self._cropper.getCroppedCanvas({
width: self._width,
height: self._height,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
}).toDataURL('image/png')
const file = new File(
[self.dataURLtoBlob(self._value)],
self.__originalFileName, {type: 'image/png', lastModified: new Date() })
container.items.add(file)
self.$refs.actualImage.files = container.files
}
}
}
</script>
<style lang="scss" scopped>
@import '~cropperjs/dist/cropper.css';
@import '../../../../node_modules/bootstrap/scss/bootstrap-utilities.scss';
.ImageInput {
/* Ensure the size of the image fit the container perfectly */
.imageModification {
display: block;
/* This rule is very important, please don't ignore this */
max-width: 100%;
}
.rotateButton {
display: flex;
flex-direction: column;
}
.rotateDrag {
display: flex;
flex-direction: column-reverse;
width: 100%;
& > input {
width: 100%;
}
}
.previewIMG {
position: relative;
& > div {
position: relative;
overflow: hidden;
border: 1px solid black;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
@include media-breakpoint-up (md) {
& > div {
position: absolute;
}
}
}
input.rightAligned {
direction:ltr !important;
overflow:hidden !important;
}
input.rightAligned:not(:focus) {
direction:rtl !important;
text-align: left !important;
text-overflow: ellipsis !important;
}
}
</style>