feat: ✨ config upload image for take SVG

This commit is contained in:
Abdel LAMRANI 2026-06-19 17:27:21 +02:00
parent 7369d3c820
commit 9d081727bc
5 changed files with 71 additions and 37 deletions

View file

@ -50,7 +50,7 @@ class SecureHeaders
"default-src 'none';",
"manifest-src 'self';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob: " . implode(' ', [
"connect-src 'self' blob: data: " . implode(' ', [
// * VITE DEV
config('app.debug') ? "wss://{$host}:4443" : '',
config('app.debug') ? "https://{$host}:4443" : '',

View file

@ -27,6 +27,7 @@ return [
'validate' => 'Validate',
'preview' => 'Preview',
'remove_picture' => 'Remove picture',
'svg_not_croppable' => 'SVGs do not require cropping'
],
'WordCounterComponent' => [
'words' => 'Words',

View file

@ -27,6 +27,7 @@ return [
'validate' => 'Valider',
'preview' => 'Prévisualisation',
'remove_picture' => 'Supprimer l\'image',
'svg_not_croppable' => 'Les SVG ne nécessitent pas de recadrage'
],
'WordCounterComponent' => [
'words' => 'Mots',

View file

@ -32,7 +32,7 @@
type="file"
class="d-none"
:name="parameters.name"
accept=".jpg,.jpeg,.gif,.png"
accept=".jpg,.jpeg,.gif,.png,.svg"
:aria-describedby="`Help${parameters.id}`"
>
<input
@ -77,22 +77,24 @@
>
<FontAwesomeIcon icon="fa-solid fa-eye" />
</span>
</button>
<button
@click.prevent="prepareImageEditor()"
:disabled="!hasPicture"
:class="`btn ${!hasCroppedPicture ? 'btn-primary' : 'btn-success'}`"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#Modal${parameters.id}`"
</button>
<span
data-bs-toggle="tooltip"
:title="isCurrentFileSvg
? trans.methods.__('vue.ImageInputComponent.svg_not_croppable')
: trans.methods.__('vue.ImageInputComponent.resize_source_picture')"
>
<span
data-bs-toggle="tooltip"
:title="trans.methods.__('vue.ImageInputComponent.resize_source_picture')"
<button
@click.prevent="prepareImageEditor()"
:disabled="!hasPicture || isCurrentFileSvg"
:class="`btn ${!hasCroppedPicture ? 'btn-primary' : 'btn-success'}`"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#Modal${parameters.id}`"
>
<FontAwesomeIcon icon="fa-solid fa-crop" />
</span>
</button>
</button>
</span>
<span
v-if="hasCroppedPicture"
class="input-group-text text-success"
@ -427,6 +429,7 @@ import Cropper from "cropperjs";
import { computed, defineOptions, nextTick, onMounted, reactive, ref, useAttrs, watch } from "vue";
import { Tooltips } from "./../../modules/bsTooltips";
import trans from "./../../modules/trans";
import { Tooltip } from "bootstrap";
/** If string the it is an absolute file path. */
type FileOutputValue = File|string|null;
@ -555,6 +558,14 @@ const parameters = reactive<{
},
});
const isCurrentFileSvg = ref<boolean>(false);
/** Detect SVG by mime type or extension */
function isSvgFile(file: File): boolean {
return (
file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg")
);
}
/** Internal file we are working on. */
const fileParameters = reactive<{
file: File|null,
@ -666,7 +677,10 @@ watch(previousPropValue, () => {
}
cropperReset();
hasCroppedPicture.value = false;
fetchPictureFromPath(fileParameters.filePath)!.then((file) => useThis(file));
const fetchPromise = fetchPictureFromPath(fileParameters.filePath);
if (fetchPromise) {
fetchPromise.then((file) => useThis(file));
}
});
/** Crop parameters changes. */
watch(cropperParameters, (newValue: CropperParameters) => {
@ -700,6 +714,11 @@ watch(fileDataUrl, (newValue) => {
}
fileParameters.dimensions = {...parameters.dimensions};
});
watch(isCurrentFileSvg, () => {
nextTick(() => {
tooltips.value?.refreshTooltips();
});
});
// * METHODS
function assignFromProps(): void {
@ -777,10 +796,12 @@ function chooseAFile(): void {
/** Input file has changed. */
function pictureInputHasChanged(): void {
if (inputFileImage?.value?.files && inputFileImage!.value!.files[0]) {
const file = inputFileImage.value.files[0];
cropperReset();
hasCroppedPicture.value = false;
useThis(inputFileImage!.value!.files[0]);
fileOutput.value = inputFileImage!.value!.files[0];
isCurrentFileSvg.value = isSvgFile(file);
useThis(file);
fileOutput.value = file;
}
}
@ -793,17 +814,19 @@ function fetchPictureFromPath(filePath: string): Promise<File>|void {
const prefixedFilePath: string = filePath.replace(/^\/|^/, "/"),
stringFileName: string = prefixedFilePath.substring(prefixedFilePath.lastIndexOf("/")).replace(/^\/|^/, "");
fetch(prefixedFilePath)
.then((response) => response.blob())
.then((blob: Blob) => {
const file = new File([blob], stringFileName, {
type: "image/png",
lastModified: new Date().getTime(),
});
setFileAsDataUrl(file);
accept(file);
}).catch(() => deny("Failed to fetch picture"));
}));
fetch(prefixedFilePath)
.then((response) => response.blob())
.then((blob: Blob) => {
const isSvg = stringFileName.toLowerCase().endsWith(".svg");
isCurrentFileSvg.value = isSvg;
const file = new File([blob], stringFileName, {
type: isSvg ? "image/svg+xml" : blob.type || "image/png",
lastModified: new Date().getTime(),
});
setFileAsDataUrl(file);
accept(file);
}).catch(() => deny("Failed to fetch picture"));
}));
}
/** Persist a File added from input or while component init. */
@ -868,6 +891,7 @@ function resetComponent(): void {
fileDataUrl.value = null;
fileOutput.value = null;
inputFileImage.value!.value = "";
isCurrentFileSvg.value = false;
fileParameters.fileName = trans.methods.__("vue.ImageInputComponent.choose_a_file");
fileParameters.filePath = "";
fileParameters.file = null;

View file

@ -366,15 +366,14 @@
<template v-if="imageLoaded">
<ImageInputComponent
v-model="options.image"
:id="'logo'"
:name="'logo'"
:helper="'L\'image qui va servir de vignette au produit, doit être de 500px (largeur) par 500px (hauteur) maximum'"
:width="100"
:height="100"
:required="false"
:value="options.image"
@image-file="updatePicture()"
:value="logoStoredPath"
@image-file="updatePicture"
/>
</template>
<input
@ -428,8 +427,10 @@ export default defineComponent({
this.options = data.settings;
}
if(data.logo !== "") {
this.logoStoredPath = data.logo;
this.options.image = "/" + data.logo;
} else {
this.logoStoredPath = "";
this.options.image = "";
}
}
@ -444,15 +445,20 @@ export default defineComponent({
},
methods: {
download() {
this.qrCode.update(this.options);
this.qrCode.download({ extension: this.extension as FileExtension });
},
updatePicture() {
const el = document.getElementById("PreviewHelplogo")?.parentElement?.children[1] as HTMLElement;
if (el instanceof HTMLElement) {
this.options.image = el.getAttribute("src") as string;
updatePicture(file: File | null) {
if (!file) {
this.options.image = "";
this.qrCode.update(this.options);
return;
}
const reader = new FileReader();
reader.onload = () => {
this.options.image = String(reader.result);
this.qrCode.update(this.options);
};
reader.readAsDataURL(file);
},
changeColor(color: string) {
if (!this.isTransparent) {
@ -483,6 +489,7 @@ export default defineComponent({
urlInput: HTMLInputElement,
urlInputValue: string,
imageLoaded: boolean,
logoStoredPath: string,
options: {
type: DrawType;
width: number;
@ -528,6 +535,7 @@ export default defineComponent({
urlInput: document.getElementById(this.urlInputId) as HTMLInputElement,
urlInputValue: "",
imageLoaded: false,
logoStoredPath: "",
options: {
width: 300,
height: 300,