ldc-le-gaulois-pro/resources/ts/components/bo/ImagesInputComponent.vue
2023-01-16 15:34:52 +01:00

1028 lines
31 KiB
Vue

<template>
<div :class="`container images-input-component images-input-${intId} p-0`">
<!-- Main buttons -->
<div class="row">
<div class="col-12 d-flex form-group">
<div class="input-group">
<button
@click.prevent="chooseFiles"
class="btn btn-outline-secondary"
type="button"
:title="__('Modifier les images sources')"
data-bs-tooltip="tooltip"
:disabled="intValues.length >= itemLimit[1]"
>
<i class="fa-solid fa-folder-open" />
</button>
<input
:id="intId"
ref="actualImages"
@change="changedImages"
type="file"
class="d-none"
name="multipleInputImages"
accept=".jpg,.jpeg,.gif,.png"
:aria-describedby="`Help${intId}`"
multiple
>
<input
@click.prevent="chooseFiles"
type="text"
:value="
'Actuellement ' +
intValues.length +
'/' +
itemLimit[1] +
' images sont présentent'
"
class="form-control right-aligned"
:title="__('Modifier les images sources')"
data-bs-tooltip="tooltip"
:aria-describedby="`Help${intId}`"
:disabled="intValues.length >= itemLimit[1]"
readonly
>
<button
class="btn btn-primary btn-collapse collapsed"
type="button"
:title="__('Visualiser/cacher l\'ensemble des images')"
data-bs-tooltip="tooltip"
data-bs-toggle="collapse"
data-bs-target="#multiple-images"
aria-expanded="false"
aria-controls="multiple-images"
:disabled="!intHasImages"
>
<i class="fa fa-arrow-down" />
</button>
<button
v-if="intValues.length > itemLimit[0]"
@click.prevent="removeFiles"
class="btn btn-danger"
type="button"
:title="__('Supprimer toutes les images')"
data-bs-tooltip="tooltip"
:disabled="intValues.length <= 0"
>
<i class="fas fa-eraser" />
</button>
</div>
</div>
</div>
<!-- End main buttons -->
<!-- Content collapse -->
<div
class="collapse"
id="multiple-images"
>
<div class="row w-100 mx-auto my-2">
<div
v-for="n = 0 in intValues.length"
:key="n"
class="col-12 col-sm-6 col-xl-4 col-xxl-3 form-group text-center"
:data-test="`test${n}`"
>
<label
v-if="intValues[n - 1]"
class="col-form-label w-100 fw-bold w-100"
>
{{ __("Image n°") }}{{ n }}
</label>
<img
v-if="intValues[n - 1]"
class="img-fluid"
:src="intValues[n - 1]"
:alt="__('Image placeholder')"
:aria-describedby="`PreviewHelp${intId}`"
>
<p class="text-danger m-0">
{{
parseValidationInput(
`${intName.replace(/[\[\]']+/g, "")}.${n - 1}`,
allErrors
)
}}
</p>
<small
v-if="intValues[n - 1]"
:id="`PreviewHelp${intId}`"
class="form-text text-muted d-block m-0"
>
<span v-if="intOriginalFile[n - 1]">
{{ humanFileSize(intOriginalFile[n - 1].size) }}
{{ `largeur ${intOriginalFileDimensions[n - 1].width}px` }}
{{ `hauteur ${intOriginalFileDimensions[n - 1].height}px` }}.
</span>
</small>
<div class="input-group justify-content-center mt-1">
<button
@click.prevent="chooseAFile(n - 1)"
class="btn btn-outline-secondary"
type="button"
:title="__('Modifier l\'image source')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-folder-open" />
</button>
<input
:id="intId"
ref="actualImage"
@change="changedImage($event, n - 1)"
type="file"
class="d-none"
:name="intName"
accept=".jpg,.jpeg,.gif,.png"
:aria-describedby="`Help${intId}`"
>
<button
@click.prevent="prepareImageEditor(n - 1)"
:disabled="!intHasImage[n - 1]"
:class="`btn ${
!intHasModdedImage[n - 1] ? 'btn-primary' : 'btn-success'
}`"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#Modal${intId}`"
:title="__('Redimensionner l\'image')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-crop" />
</button>
<span
v-if="intHasModdedImage[n - 1]"
class="input-group-text text-success"
:title="__('Image redimensionée')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-wand-magic" />
</span>
<button
v-if="intValues.length > itemLimit[0]"
@click.prevent="removeFile(n - 1)"
class="btn btn-outline-danger"
type="button"
:title="__('Supprimer l\'image')"
data-bs-tooltip="tooltip"
>
<i class="fas fa-trash" />
</button>
</div>
</div>
</div>
<!-- Cropper modal -->
<div
:id="`Modal${intId}`"
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')"
:title="__('Fermer la fenêtre sans valider le contenu')"
data-bs-tooltip="tooltip"
/>
</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"
:title="__('Réinitialiser l\'image')"
data-bs-tooltip="tooltip"
@click.prevent="reset()"
>
<i class="fa fa-sync-alt" />
</button>
</div>
<div class="btn-group me-4 mt-2">
<button
class="btn btn-primary"
:title="__('Zoom avant')"
data-bs-tooltip="tooltip"
@click.prevent="zoom(0.1)"
>
<i class="fa fa-search-plus" />
</button>
<a
class="btn btn-primary"
:title="__('Zoom arrière')"
data-bs-tooltip="tooltip"
@click.prevent="zoom(-0.1)"
>
<i class="fa fa-search-minus" />
</a>
</div>
<div class="btn-group me-4 mt-2">
<a
class="btn btn-primary"
:title="__('Déplacer vers la gauche')"
data-bs-tooltip="tooltip"
@click.prevent="move(-10, 0)"
>
<i class="fa fa-arrow-left" />
</a>
<a
class="btn btn-primary"
:title="__('Déplacer vers la droite')"
data-bs-tooltip="tooltip"
@click.prevent="move(10, 0)"
>
<i class="fa fa-arrow-right" />
</a>
<a
class="btn btn-primary"
data-bs-tooltip="tooltip"
:title="__('Déplacer vers le haut')"
@click.prevent="move(0, -10)"
>
<i class="fa fa-arrow-up" />
</a>
<a
class="btn btn-primary"
:title="__('Déplacer vers le bas')"
data-bs-tooltip="tooltip"
@click.prevent="move(0, 10)"
>
<i class="fa fa-arrow-down" />
</a>
</div>
<div class="btn-group me-4 mt-2">
<a
class="btn btn-primary"
:title="__('Mirroir Horizontal')"
data-bs-tooltip="tooltip"
@click.prevent="scale(true)"
>
<i class="fa fa-arrows-alt-h" />
</a>
<a
class="btn btn-primary"
:title="__('Mirroir Vertical')"
data-bs-tooltip="tooltip"
@click.prevent="scale(false)"
>
<i class="fa fa-arrows-alt-v" />
</a>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex flex-row">
<div class="rotate-button">
<span class="text-dark">{{ __("Rotation") }}&nbsp;:</span>
<div class="btn-group mt-1">
<a
class="btn btn-primary"
:title="__('Rotation sens anti-horaire')"
data-bs-tooltip="tooltip"
@click.prevent="rotate(-45)"
>
<i class="fa fa-undo-alt" />
</a>
<a
class="btn btn-primary"
:title="__('Rotation sens horaire')"
data-bs-tooltip="tooltip"
@click.prevent="rotate(45)"
>
<i class="fa fa-redo-alt" />
</a>
</div>
</div>
<div class="mt-1 ms-4 rotate-drag">
<label
for="range"
class="form-label me-4"
>{{
__(":deg degrès", {
deg: String(intRotationText),
})
}}
</label>
<input
ref="rotationSlider"
@input.stop="rotationChange($event as Event)"
@change.stop="
rotateTo(
Number.parseInt(
($event.target as HTMLInputElement).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 col-md-6 p-0 mb-3">
<img
class="image-modification"
ref="imgEditor"
src=""
>
</div>
<div
ref="previewBox"
class="col-12 col-md-6 p-0 bg-light preview-image"
>
<div
:id="`Preview${intId}`"
:style="intPreviewStyle"
/>
</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')"
:title="__('Fermer la fenêtre sans valider le contenu')"
data-bs-tooltip="tooltip"
>
{{ __("Fermer") }}
</button>
<button
@click.prevent="exportToBlob"
type="button"
class="btn btn-primary"
:title="__('Valider le contenu et fermer la fenêtre')"
data-bs-tooltip="tooltip"
data-bs-dismiss="modal"
>
{{ __("Valider") }}
</button>
</div>
</div>
</div>
</div>
<!-- End cropper modal -->
</div>
<!-- End content collapse -->
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Cropper from "cropperjs";
import { Tooltip } from "bootstrap";
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
import trans from "../../modules/trans";
const UNITS = [
"byte",
"kilobyte",
"megabyte",
"gigabyte",
"terabyte",
"petabyte",
];
const BYTES_PER_KB = 1024;
export default defineComponent({
mixins: [parseLaravelErrorMessages, trans],
props: {
id: {
type: String,
default: "",
},
required: {
type: Boolean,
default: false,
},
name: {
type: String,
default: "",
},
width: {
type: Number,
default: 0,
},
height: {
type: Number,
default: 0,
},
value: {
type: String,
default: "",
},
preview: {
type: Boolean,
default: true,
},
},
data(): {
intId: string;
intName: string;
intWidth: number;
intHeight: number;
intRotationText: number;
intRotationValue: number;
intScaleX: number;
intScaleY: number;
intHasImage: boolean[];
intHasModdedImage: boolean[];
intCropper: Cropper | null;
intOriginalFile: File[];
intOriginalFileDimensions: {
height: number;
width: number;
}[];
intOriginalFileName: string[];
intPreviewStyle: string;
intIdImage: number;
intHasImages: boolean;
intValues: string[];
intTooltipList: HTMLButtonElement[];
intBtnCollapse: HTMLButtonElement | null;
intLoopLoadImages: number;
intInputImages: HTMLInputElement[];
itemLimit: number[];
allErrors: Record<string, string[]>;
} {
return {
intId: "",
intName: "",
intWidth: 0,
intHeight: 0,
intRotationText: 0,
intRotationValue: 0,
intScaleX: 1,
intScaleY: 1,
intHasImage: [],
intHasModdedImage: [],
intCropper: null,
intOriginalFile: [],
intOriginalFileDimensions: [],
intOriginalFileName: [],
intPreviewStyle: "",
intIdImage: 0,
intHasImages: false,
intValues: [],
intBtnCollapse: null,
intTooltipList: [],
intLoopLoadImages: 0,
intInputImages: [],
itemLimit: [0, 0],
allErrors: {},
};
},
mounted() {
const json = String(this.$attrs.json ?? "");
const data = JSON.parse(json);
this.intId = data.id;
this.intName = data.name;
this.intWidth = data.width;
this.intHeight = data.height;
this.intValues = data.value;
this.itemLimit = data.limit;
this.allErrors = data.errors ?? {};
this.$nextTick(() => {
this.initComponent();
this.updateBootstrapTooltip();
});
},
computed: {
imageRatio() {
return this.intWidth / this.intHeight;
},
cropperOption(): Cropper.Options {
return {
autoCrop: true,
background: true,
autoCropArea: 1,
viewMode: 0,
dragMode: "move",
movable: true,
rotatable: true,
scalable: true,
zoomable: true,
zoomOnTouch: false,
zoomOnWheel: false,
cropBoxMovable: true,
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
aspectRatio: this.intWidth / this.intHeight,
preview: `#Preview${this.intId}`,
};
},
},
watch: {
intRotationValue(value) {
if (!(this.intCropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
this.intCropper.rotateTo(value);
},
},
methods: {
/**
* Initialise the component with previously images registered.
*/
initComponent() {
if (this.intValues.length > 0) {
this.intBtnCollapse = document.querySelector(
"[data-bs-target=\"#multiple-images\"]"
);
this.intValues.forEach((element, index) => {
element.charAt(0) != "/" ? "/".concat(element) : "";
this.intValues.splice(index, 1, element.replace(/^\/|^/, "/"));
});
this.intHasImage = Array(this.intValues.length).fill(false);
this.intOriginalFileDimensions = Array(this.intValues.length).fill({
width: 0,
height: 0,
});
this.intHasImages = true;
this.initImagesSaved();
} else {
this.intHasImages = false;
}
},
/**
* Assign previously images registered to respective input.
*/
initImagesSaved() {
var actualImages = this.$refs.actualImage as HTMLInputElement[];
this.intValues.forEach((element, index) => {
this.createFile(element, "image_" + index + ".png", "image/png").then(
(file: File) => {
const container = new DataTransfer();
container.items.add(file);
actualImages[index].files = container.files;
}
);
});
},
/**
* Create a file from a path.
*/
async createFile(path: string, name: string, type: string): Promise<File> {
let response = await fetch(
window.location.protocol + "//" + window.location.host + "/" + path
);
let data = await response.blob();
let metadata = {
type: type,
};
return new File([data], name, metadata);
},
/**
* Update Bootstrap tooltips.
*/
updateBootstrapTooltip() {
let newTooltipList = [].slice.call(
document.querySelectorAll(
".images-input-" + this.intId + " [data-bs-tooltip=\"tooltip\"]"
)
) as HTMLButtonElement[];
let tmp = newTooltipList.filter((x) => !this.intTooltipList.includes(x));
tmp.map((tooltip) => {
return new Tooltip(tooltip);
});
this.intTooltipList = newTooltipList;
this.closeBootstrapTooltip();
},
/**
* Close all Bootstrap tooltips.
*/
closeBootstrapTooltip() {
this.intTooltipList.forEach((tooltip) => {
tooltip.blur();
Tooltip.getInstance(tooltip)?.hide();
});
},
/**
* Cropper preview style.
*/
previewStyleCompute() {
const previewBox = this.$refs.previewBox as HTMLElement,
style = window.getComputedStyle(previewBox),
targetWidth = Number.parseFloat(style.width) / 1.2,
targetHeight =
(this.intHeight * Number.parseFloat(style.width)) /
this.intWidth /
1.2;
this.intPreviewStyle = `width: ${targetWidth.toFixed(
3
)}px;height:${targetHeight.toFixed(3)}px;`;
},
/**
* Cropper reset.
*/
reset() {
if (!(this.intCropper instanceof Cropper)) {
return;
}
this.intCropper.reset();
},
/**
* Cropper zoom.
*/
zoom(v: number) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
this.intCropper.zoom(v);
},
/**
* Cropper move.
*/
move(x: number, y: number) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
this.intCropper.move(x, y);
},
/**
* Cropper scale.
*/
scale(horizontal: boolean) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
if (horizontal) {
this.intScaleX = -1 / this.intScaleX;
this.intCropper.scaleX(this.intScaleX);
} else {
this.intScaleY = -1 / this.intScaleY;
this.intCropper.scaleY(this.intScaleY);
}
},
/**
* Cropper rotate.
*/
rotate(value: number) {
const rotationSlider = this.$refs.rotationSlider as HTMLInputElement;
let res = this.intRotationValue + value;
res = res < 0 ? 0 : res;
res = res > 360 ? 360 : res;
this.intRotationValue = res;
this.intRotationText = this.intRotationValue;
rotationSlider.value = String(this.intRotationValue);
},
/**
* Cropper rotate to.
*/
rotateTo(value: number) {
value = value < 0 ? 0 : value;
value = value > 360 ? 360 : value;
this.intRotationValue = value;
this.intRotationText = this.intRotationValue;
},
/**
* Cropper rotation input.
*/
rotationChange(e: Event) {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error("target element is not an input");
}
this.rotateTo(Number.parseFloat(el.value));
},
/**
* Link button change image to the input file image.
*/
chooseAFile(n: number) {
var actualImages = this.$refs.actualImage as HTMLInputElement[],
actualImage = actualImages[n] as HTMLInputElement;
actualImage.click();
},
/**
* Link button change images to the input file images.
*/
chooseFiles() {
const actualImages = this.$refs.actualImages as HTMLInputElement;
actualImages.click();
},
/**
* Remove one specific image.
*/
removeFile(n: number) {
this.intValues.splice(n, 1);
this.intHasImage.splice(n, 1);
this.intHasModdedImage.splice(n, 1);
this.intOriginalFile.splice(n, 1);
this.intOriginalFileName.splice(n, 1);
this.intOriginalFileDimensions.splice(n, 1);
if (this.intValues.length <= 0) {
this.intHasImages = false;
}
this.intInputImages = this.$refs.actualImage as HTMLInputElement[];
this.intInputImages.forEach((element, index) => {
if (this.intOriginalFile[index] != undefined) {
var dt = new DataTransfer();
dt.items.add(this.intOriginalFile[index]);
element.files = dt.files;
}
});
this.closeBootstrapTooltip();
},
/**
* Remove all images.
*/
removeFiles() {
this.intValues.length = this.itemLimit[0];
this.intHasImage.length = this.itemLimit[0];
this.intHasModdedImage.length = this.itemLimit[0];
this.intOriginalFile.length = this.itemLimit[0];
this.intOriginalFileName.length = this.itemLimit[0];
this.intOriginalFileDimensions.length = this.itemLimit[0];
this.itemLimit[0] > 0
? (this.intHasImages = true)
: (this.intHasImages = false);
this.allErrors = {};
this.closeBootstrapTooltip();
},
/**
* Change one specific image.
*/
changedImage(e: Event, n: number) {
const reader = new FileReader();
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
if (!el.files) {
throw new Error();
}
this.intHasImage[n] = true;
this.intHasModdedImage[n] = false;
reader.addEventListener(
"load",
() => {
// On convertit l'image en une chaîne de caractères base64.
this.intValues[n] = String(reader.result);
},
false
);
if (el.files[0]) {
this.intOriginalFile[n] = el.files[0];
reader.readAsDataURL(el.files[0]);
reader.onloadend = () =>
this.fetchDataUrlImageSizes(String(reader.result), n);
}
},
/**
* Add images to exists image(s).
*/
changedImages(e: Event) {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
if (!el.files) {
throw new Error();
}
this.intLoopLoadImages = this.intValues.length;
this.loadImages([].slice.call(el.files));
this.intHasImages = true;
if (this.intBtnCollapse?.classList.contains("collapsed")) {
this.intBtnCollapse?.click();
}
},
/**
* Update inputs file.
*/
loadImages(files: Array<File>) {
if (files.length > 0) {
if (this.intValues.length + files.length <= this.itemLimit[1]) {
this.intOriginalFileDimensions.push({ width: 0, height: 0 });
let actualFile = files.shift() as File;
const reader = new FileReader();
reader.readAsDataURL(actualFile);
reader.onload = () => {
// On convertit l'image en une chaîne de caractères base64.
this.intValues.push(String(reader.result));
};
reader.onloadend = () => {
this.fetchDataUrlImageSizes(
String(reader.result),
this.intLoopLoadImages
);
this.intInputImages = this.$refs.actualImage as HTMLInputElement[];
var dt = new DataTransfer();
dt.items.add(actualFile);
this.intInputImages[this.intLoopLoadImages].files = dt.files;
this.intOriginalFile[this.intLoopLoadImages] = actualFile;
this.intHasImage[this.intValues.length - 1] = true;
this.intLoopLoadImages++;
this.loadImages(files);
};
}
} else {
this.updateBootstrapTooltip();
this.intLoopLoadImages = this.intValues.length;
}
},
/**
* Set cropper to edit a specific image.
*/
prepareImageEditor(n: number) {
const imgEditor = this.$refs.imgEditor as HTMLImageElement,
createCropper = () => {
if (this.intCropper) {
this.intCropper.destroy();
}
this.intCropper = new Cropper(imgEditor, this.cropperOption);
};
var actualImages = this.$refs.actualImage as HTMLInputElement[],
actualImage = actualImages[n] as HTMLInputElement;
this.intIdImage = n;
if (!actualImage || !actualImage.files) {
throw new Error("actualImage is not defined");
}
if (actualImage.files[0]) {
this.intOriginalFile[n] = actualImage.files[0];
this.intOriginalFileName[n] = actualImage.files[0].name;
imgEditor.src = URL.createObjectURL(actualImage.files[0]);
}
this.previewStyleCompute();
this.$nextTick(() => createCropper());
return;
},
/**
* Create a Blob from a specific url.
*/
dataURLtoBlob(dataUrl: string) {
const arr = dataUrl.split(",");
if (!arr[0] || !arr[1]) {
throw new Error("invalid data url");
}
const mime = arr[0].match(/:(.*?);/);
if (!mime || mime.length < 2) {
throw new Error("invalid mime");
}
const bstr = window.atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime[1] });
},
/**
* Update image sizes.
*/
fetchDataUrlImageSizes(dataUrl: string, n: number) {
const img = new Image();
try {
img.onload = () => {
this.intOriginalFileDimensions[n].width = img.width;
this.intOriginalFileDimensions[n].height = img.height;
};
img.src = dataUrl;
} catch (e) {
this.intOriginalFileDimensions[n].width = img.width;
this.intOriginalFileDimensions[n].height = img.height;
}
},
/**
* Set image after the edit in cropper.
*/
exportToBlob() {
const container = new DataTransfer();
var actualImages = this.$refs.actualImage as HTMLInputElement[],
actualImage = actualImages[this.intIdImage] as HTMLInputElement;
if (!(this.intCropper instanceof Cropper)) {
return;
}
if (!actualImage.files) {
throw new Error();
}
this.intHasModdedImage[this.intIdImage] = true;
this.intValues[this.intIdImage] = this.intCropper
.getCroppedCanvas({
fillColor: "rgba(0, 0, 0, 0)",
width: this.intWidth,
height: this.intHeight,
imageSmoothingEnabled: true,
imageSmoothingQuality: "high",
})
.toDataURL("image/png");
this.fetchDataUrlImageSizes(
this.intValues[this.intIdImage],
this.intIdImage
);
const file = new File(
[this.dataURLtoBlob("/" + this.intValues[this.intIdImage])],
this.intOriginalFileName[this.intIdImage],
{
type: "image/png",
lastModified: new Date().getTime(),
}
);
this.intOriginalFile[this.intIdImage] = file;
container.items.add(file);
actualImage.files = container.files;
this.$nextTick(() => {
this.updateBootstrapTooltip();
});
},
/**
* Get size of a file.
*/
humanFileSize(sizeBytes: number | bigint): string {
let size = Math.abs(Number(sizeBytes));
let u = 0;
while (size >= BYTES_PER_KB && u < UNITS.length - 1) {
size /= BYTES_PER_KB;
++u;
}
return new Intl.NumberFormat([], {
style: "unit",
unit: UNITS[u],
unitDisplay: "short",
maximumFractionDigits: 1,
}).format(size);
},
},
});
</script>
<style lang="scss">
.images-input-component {
.image-modification {
display: block;
max-width: 100%;
}
.rotate-button {
display: flex;
flex-direction: column;
}
.rotate-drag {
display: flex;
width: 100%;
flex-direction: column-reverse;
input {
width: 100%;
}
}
.preview-image {
position: relative;
div {
position: relative;
top: 50%;
left: 50%;
overflow: hidden;
border: 1px solid #000;
transform: translateX(-50%) translateY(-50%);
}
}
.right-aligned {
overflow: hidden !important;
direction: ltr !important;
}
.right-aligned:not(:focus) {
text-align: left !important;
text-overflow: ellipsis !important;
}
.btn-collapse svg {
transform: rotate(180deg);
transition: 0.3s;
}
.collapsed {
svg {
transform: rotate(0deg);
}
}
#multiple-images .row {
max-height: 30rem;
overflow-y: auto;
}
}
</style>