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
337 lines
15 KiB
Vue
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') }} :</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>
|