ldc-loue/resources/js/components/bo/ImageInputComponent.vue
Jérémy Munsch 0751114f4a
BO News, Tags, Sliders
CKeditor + word counter + Image Input + Taggable VUE3
Better Xss Sanitizer
Handle Video Youtube + Image Upload + Inline Image from url
Better Image Input component with reset, easizer resize
Full Responsive BO
2022-06-04 15:55:36 +02:00

337 lines
15 KiB
Vue

<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="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`">
<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>
</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>
</div>
<div :id="`${id}Modal`" 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" @input="rotationChange($event)" @change="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="`${id}Preview`" :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>
</template>
<script>
import Cropper from "cropperjs/dist/cropper.js"
export default {
mounted() {
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(data.required !== undefined) {
this.required = data.required ? true : false
}
},
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: `#${this.id}Preview`
}
}
},
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)
}
},
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) {
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() {
const image = this.$refs.actualImage.files[0]
this.$refs.imageModification.src = URL.createObjectURL(image)
if(this.cropper) {
this.cropper.destroy()
}
this.cropper = new Cropper(this.$refs.imageModification, this.cropperOption)
},
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 originalFile = self.$refs.actualImage.files[0]
const container = new DataTransfer()
this.value = this.cropper.getCroppedCanvas({
width: this.width,
height: this.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() })
container.items.add(file)
this.$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>