|
@@ -1,292 +1,494 @@
|
|
|
<!--
|
|
<!--
|
|
|
Assistant de création d'image
|
|
Assistant de création d'image
|
|
|
-https://norserium.github.io/vue-advanced-cropper/
|
|
|
|
|
|
|
+
|
|
|
|
|
+@see https://norserium.github.io/vue-advanced-cropper/
|
|
|
-->
|
|
-->
|
|
|
<template>
|
|
<template>
|
|
|
- <LazyLayoutDialog :show="true">
|
|
|
|
|
|
|
+ <div class="input-image">
|
|
|
|
|
+ <UiImage
|
|
|
|
|
+ ref="uiImage"
|
|
|
|
|
+ :image-id="modelValue"
|
|
|
|
|
+ :default-image="defaultImage"
|
|
|
|
|
+ :width="width"
|
|
|
|
|
+ :height="height"
|
|
|
|
|
+ class="image"
|
|
|
|
|
+ overlay-icon="fas fa-upload"
|
|
|
|
|
+ @overlay-clicked="openModal()"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <LazyLayoutDialog :show="showModal">
|
|
|
<template #dialogType>{{ $t('image_assistant') }}</template>
|
|
<template #dialogType>{{ $t('image_assistant') }}</template>
|
|
|
<template #dialogTitle>{{ $t('modif_picture') }}</template>
|
|
<template #dialogTitle>{{ $t('modif_picture') }}</template>
|
|
|
<template #dialogText>
|
|
<template #dialogText>
|
|
|
<div class="upload">
|
|
<div class="upload">
|
|
|
<v-row
|
|
<v-row
|
|
|
- v-if="fetchState.pending"
|
|
|
|
|
|
|
+ v-if="pending"
|
|
|
class="fill-height ma-0 loading"
|
|
class="fill-height ma-0 loading"
|
|
|
align="center"
|
|
align="center"
|
|
|
justify="center"
|
|
justify="center"
|
|
|
>
|
|
>
|
|
|
- <v-progress-circular
|
|
|
|
|
- :indeterminate="true"
|
|
|
|
|
- color="neutral">
|
|
|
|
|
|
|
+ <v-progress-circular :indeterminate="true" color="neutral">
|
|
|
</v-progress-circular>
|
|
</v-progress-circular>
|
|
|
</v-row>
|
|
</v-row>
|
|
|
|
|
|
|
|
- <div v-else >
|
|
|
|
|
|
|
+ <div v-else>
|
|
|
<div class="upload__cropper-wrapper">
|
|
<div class="upload__cropper-wrapper">
|
|
|
- <cropper
|
|
|
|
|
|
|
+ <Cropper
|
|
|
ref="cropper"
|
|
ref="cropper"
|
|
|
class="upload__cropper"
|
|
class="upload__cropper"
|
|
|
check-orientation
|
|
check-orientation
|
|
|
- :src="image.src"
|
|
|
|
|
- :default-position="{left : coordinates.left, top : coordinates.top}"
|
|
|
|
|
- :default-size="coordinates.width ? {width : coordinates.width, height : coordinates.height}: defaultSize"
|
|
|
|
|
- @change="onChange"
|
|
|
|
|
|
|
+ :src="currentImage.src"
|
|
|
|
|
+ :default-position="defaultPosition"
|
|
|
|
|
+ :default-size="defaultSize"
|
|
|
|
|
+ @change="onCropperChange"
|
|
|
/>
|
|
/>
|
|
|
- <div v-if="image.src" class="upload__reset-button" title="Reset Image" @click="reset()">
|
|
|
|
|
- <v-icon>mdi-delete</v-icon>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="currentImage.src"
|
|
|
|
|
+ class="upload__reset-button"
|
|
|
|
|
+ title="Reset Image"
|
|
|
|
|
+ @click="reset()"
|
|
|
|
|
+ >
|
|
|
|
|
+ <v-icon>fas fa-trash</v-icon>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
<div class="upload__buttons-wrapper">
|
|
<div class="upload__buttons-wrapper">
|
|
|
- <button class="upload__button" @click="$refs.file.click()">
|
|
|
|
|
- <input ref="file" type="file" accept="image/*" @change="uploadImage($event)" />
|
|
|
|
|
- {{$t('upload_image')}}
|
|
|
|
|
|
|
+ <button class="upload__button" @click="fileInput?.click()">
|
|
|
|
|
+ <input
|
|
|
|
|
+ ref="fileInput"
|
|
|
|
|
+ type="file"
|
|
|
|
|
+ accept="image/*"
|
|
|
|
|
+ @change="uploadImage($event)"
|
|
|
|
|
+ />
|
|
|
|
|
+ {{ $t('upload_image') }}
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
|
|
+ <span class="max-size-label">{{ $t('max_size_4_mb') }}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
<template #dialogBtn>
|
|
<template #dialogBtn>
|
|
|
- <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="$emit('close')">
|
|
|
|
|
|
|
+ <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="cancel">
|
|
|
{{ $t('cancel') }}
|
|
{{ $t('cancel') }}
|
|
|
</v-btn>
|
|
</v-btn>
|
|
|
- <v-btn class="mr-4 submitBtn theme-danger" @click="save">
|
|
|
|
|
|
|
+ <v-btn
|
|
|
|
|
+ class="submitBtn theme-primary"
|
|
|
|
|
+ @click="save"
|
|
|
|
|
+ :disabled="pending"
|
|
|
|
|
+ >
|
|
|
{{ $t('save') }}
|
|
{{ $t('save') }}
|
|
|
</v-btn>
|
|
</v-btn>
|
|
|
</template>
|
|
</template>
|
|
|
</LazyLayoutDialog>
|
|
</LazyLayoutDialog>
|
|
|
-
|
|
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-
|
|
|
|
|
-import {useNuxtApp} from "#app";
|
|
|
|
|
-import {ref, Ref} from "@vue/reactivity";
|
|
|
|
|
|
|
+import { Cropper } from 'vue-advanced-cropper'
|
|
|
|
|
+import 'vue-advanced-cropper/dist/style.css'
|
|
|
|
|
+import { type Ref, ref } from '@vue/reactivity'
|
|
|
import File from '~/models/Core/File'
|
|
import File from '~/models/Core/File'
|
|
|
-import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
|
|
|
|
|
-import UrlUtils from "~/services/utils/urlUtils";
|
|
|
|
|
-import {useImageFetch} from "~/composables/data/useImageFetch";
|
|
|
|
|
-import {onUnmounted, watch, WatchStopHandle} from "@vue/runtime-core";
|
|
|
|
|
-import {useEntityManager} from "~/composables/data/useEntityManager";
|
|
|
|
|
-import {AnyJson} from "~/types/data";
|
|
|
|
|
|
|
+import type { PropType } from '@vue/runtime-core'
|
|
|
|
|
+import { useEntityManager } from '~/composables/data/useEntityManager'
|
|
|
|
|
+import { useImageManager } from '~/composables/data/useImageManager'
|
|
|
|
|
+import { FILE_VISIBILITY, TYPE_ALERT } from '~/types/enum/enums'
|
|
|
|
|
+import { usePageStore } from '~/stores/page'
|
|
|
|
|
+import ImageUtils from '~/services/utils/imageUtils'
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- imageId: {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Id de l'objet File, ou null
|
|
|
|
|
+ */
|
|
|
|
|
+ modelValue: {
|
|
|
|
|
+ type: Number as PropType<number | null>,
|
|
|
|
|
+ required: false,
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Label du champ
|
|
|
|
|
+ * Si non défini, c'est le nom de propriété qui est utilisé
|
|
|
|
|
+ */
|
|
|
|
|
+ field: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ required: false,
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Image par défaut en cas d'absence d'une image uploadée
|
|
|
|
|
+ */
|
|
|
|
|
+ defaultImage: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ required: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Hauteur de l'image à l'écran (en px)
|
|
|
|
|
+ */
|
|
|
|
|
+ height: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ required: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Largeur de l'image à l'écran (en px)
|
|
|
|
|
+ */
|
|
|
|
|
+ width: {
|
|
|
type: Number,
|
|
type: Number,
|
|
|
- required: false
|
|
|
|
|
|
|
+ required: false,
|
|
|
},
|
|
},
|
|
|
|
|
+ /**
|
|
|
|
|
+ * TODO: completer
|
|
|
|
|
+ */
|
|
|
ownerId: {
|
|
ownerId: {
|
|
|
type: Number,
|
|
type: Number,
|
|
|
- required: false
|
|
|
|
|
|
|
+ required: false,
|
|
|
},
|
|
},
|
|
|
- field: {
|
|
|
|
|
- type: String,
|
|
|
|
|
- required: true
|
|
|
|
|
- }
|
|
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const { emit } = useNuxtApp()
|
|
|
|
|
-
|
|
|
|
|
const { em } = useEntityManager()
|
|
const { em } = useEntityManager()
|
|
|
|
|
+const { imageManager } = useImageManager()
|
|
|
|
|
+const pageStore = usePageStore()
|
|
|
|
|
|
|
|
-const file = new File()
|
|
|
|
|
|
|
+const emit = defineEmits(['update:modelValue'])
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Références à des composants
|
|
|
|
|
+ */
|
|
|
|
|
+const fileInput: Ref<null | any> = ref(null)
|
|
|
const cropper: Ref<any> = ref(null)
|
|
const cropper: Ref<any> = ref(null)
|
|
|
|
|
+const uiImage: Ref<any> = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * L'objet File ou l'image sont en cours de chargement
|
|
|
|
|
+ */
|
|
|
|
|
+const pending: Ref<boolean> = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Affiche la modale d'upload / modification de l'image
|
|
|
|
|
+ */
|
|
|
|
|
+const showModal = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * L'objet File contenant les informations de l'image
|
|
|
|
|
+ */
|
|
|
|
|
+const file: Ref<File | null> = ref(null)
|
|
|
|
|
|
|
|
-const image: Ref<AnyJson> = ref({
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Données d'une nouvelle image uploadée par l'utilisateur
|
|
|
|
|
+ */
|
|
|
|
|
+const currentImage: Ref<{
|
|
|
|
|
+ id: string | number | null
|
|
|
|
|
+ src: string | null
|
|
|
|
|
+ content: string | null
|
|
|
|
|
+ name: string | null
|
|
|
|
|
+}> = ref({
|
|
|
id: null,
|
|
id: null,
|
|
|
src: null,
|
|
src: null,
|
|
|
- file: null,
|
|
|
|
|
- name: null
|
|
|
|
|
|
|
+ content: null,
|
|
|
|
|
+ name: null,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const coordinates: Ref<AnyJson> = ref({})
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Taille maximale autorisée pour les images uploadées (en bytes)
|
|
|
|
|
+ */
|
|
|
|
|
+const MAX_FILE_SIZE = 4 * 1024 * 1024
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Coordonnées du cropper
|
|
|
|
|
+ */
|
|
|
|
|
+const cropperConfig: Ref<{
|
|
|
|
|
+ left?: number
|
|
|
|
|
+ top?: number
|
|
|
|
|
+ height?: number
|
|
|
|
|
+ width?: number
|
|
|
|
|
+}> = ref({})
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultposition
|
|
|
|
|
+ */
|
|
|
|
|
+const defaultPosition = () => {
|
|
|
|
|
+ return { left: cropperConfig.value.left, top: cropperConfig.value.top }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultsize
|
|
|
|
|
+ */
|
|
|
|
|
+const defaultSize = (params: any): { width: number; height: number } | null => {
|
|
|
|
|
+ if (!params) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ const { imageSize, visibleArea } = params
|
|
|
|
|
|
|
|
-const defaultSize = ({ imageSize, visibleArea }: any) => {
|
|
|
|
|
return {
|
|
return {
|
|
|
- width: (visibleArea || imageSize).width,
|
|
|
|
|
- height: (visibleArea || imageSize).height,
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ width: cropperConfig.value.width ?? (visibleArea || imageSize).width,
|
|
|
|
|
+ height: cropperConfig.value.height ?? (visibleArea || imageSize).height,
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Si l'id est renseigné, on récupère l'Item File afin d'avoir les informations de config, le nom, etc.
|
|
|
|
|
-if (props.imageId && props.imageId > 0) {
|
|
|
|
|
- const { apiRequestService } = useAp2iRequestService()
|
|
|
|
|
- const result: any = await apiRequestService.get(UrlUtils.join('api/files', '' + props.imageId))
|
|
|
|
|
-
|
|
|
|
|
- const config = JSON.parse(result.data.config)
|
|
|
|
|
- coordinates.value.left = config.x
|
|
|
|
|
- coordinates.value.top = config.y
|
|
|
|
|
- coordinates.value.height = config.height
|
|
|
|
|
- coordinates.value.width = config.width
|
|
|
|
|
- image.value.name = result.data.name
|
|
|
|
|
- image.value.id = result.data.id
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Charge l'image correspondant au fichier dans le cropper
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileId
|
|
|
|
|
+ */
|
|
|
|
|
+const loadImage = async (fileId: number) => {
|
|
|
|
|
+ file.value = (await em.fetch(File, fileId)) as File
|
|
|
|
|
+
|
|
|
|
|
+ if (file.value.config) {
|
|
|
|
|
+ const fileConfig = JSON.parse(file.value.config)
|
|
|
|
|
+ cropperConfig.value.left = fileConfig.x
|
|
|
|
|
+ cropperConfig.value.top = fileConfig.y
|
|
|
|
|
+ cropperConfig.value.height = fileConfig.height
|
|
|
|
|
+ cropperConfig.value.width = fileConfig.width
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ currentImage.value.name = file.value.name
|
|
|
|
|
+ currentImage.value.id = file.value.id
|
|
|
|
|
+ currentImage.value.src = (await imageManager.get(fileId)) as string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-//On récupère l'image...
|
|
|
|
|
-const { fetch } = useImageFetch()
|
|
|
|
|
-const { data: imageLoaded, pending } = fetch(props.imageId ?? null)
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Affiche la modale d'upload / modification de l'image
|
|
|
|
|
+ */
|
|
|
|
|
+const openModal = async () => {
|
|
|
|
|
+ showModal.value = true
|
|
|
|
|
+ pending.value = true
|
|
|
|
|
|
|
|
-const unwatch: WatchStopHandle = watch(
|
|
|
|
|
- imageLoaded,
|
|
|
|
|
-(newValue, oldValue) => {
|
|
|
|
|
- if (newValue === oldValue || typeof newValue === 'undefined') {
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- image.value.src = newValue
|
|
|
|
|
|
|
+ if (props.modelValue !== null) {
|
|
|
|
|
+ // Un objet File existe déjà: on le récupère
|
|
|
|
|
+ await loadImage(props.modelValue)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Nouveau File
|
|
|
|
|
+ file.value = em.newInstance(File) as File
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pending.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Réinitialise l'image actuellement chargée dans le cropper
|
|
|
|
|
+ */
|
|
|
|
|
+const reset = () => {
|
|
|
|
|
+ if (currentImage.value.src !== null) {
|
|
|
|
|
+ URL.revokeObjectURL(currentImage.value.src)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ currentImage.value = {
|
|
|
|
|
+ src: null,
|
|
|
|
|
+ content: null,
|
|
|
|
|
+ name: null,
|
|
|
|
|
+ id: null,
|
|
|
}
|
|
}
|
|
|
-)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ cropperConfig.value = {
|
|
|
|
|
+ left: undefined,
|
|
|
|
|
+ height: undefined,
|
|
|
|
|
+ top: undefined,
|
|
|
|
|
+ width: undefined,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Quand l'utilisateur choisit une image sur sa machine
|
|
|
|
|
|
|
+ * Upload une image depuis le poste client
|
|
|
* @param event
|
|
* @param event
|
|
|
*/
|
|
*/
|
|
|
-const uploadImage = (event:any) => {
|
|
|
|
|
|
|
+const uploadImage = async (event: any) => {
|
|
|
const { files } = event.target
|
|
const { files } = event.target
|
|
|
- if (files && files[0]) {
|
|
|
|
|
- reset()
|
|
|
|
|
- image.value.name = files[0].name
|
|
|
|
|
- image.value.src = URL.createObjectURL(files[0])
|
|
|
|
|
- image.value.file = files[0]
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (!files || !files[0]) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const uploadedFile = files[0]
|
|
|
|
|
+
|
|
|
|
|
+ if (uploadedFile.size > MAX_FILE_SIZE) {
|
|
|
|
|
+ pageStore.alerts.push({
|
|
|
|
|
+ type: TYPE_ALERT.ALERT,
|
|
|
|
|
+ messages: ['file_too_large'],
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Met à jour l'image dans le cropper
|
|
|
|
|
+ currentImage.value.id = null
|
|
|
|
|
+ currentImage.value.name = uploadedFile.name
|
|
|
|
|
+ currentImage.value.src = URL.createObjectURL(uploadedFile)
|
|
|
|
|
+ currentImage.value.content = await ImageUtils.blobToBase64(uploadedFile)
|
|
|
|
|
+
|
|
|
|
|
+ // Met à jour la configuration du cropper
|
|
|
|
|
+ cropperConfig.value.top = 0
|
|
|
|
|
+ cropperConfig.value.left = 0
|
|
|
|
|
+ cropperConfig.value.height = uploadedFile.height
|
|
|
|
|
+ cropperConfig.value.width = uploadedFile.width
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Lorsque le cropper change de position / taille, on met à jour les coordonnées
|
|
* Lorsque le cropper change de position / taille, on met à jour les coordonnées
|
|
|
- * @param config
|
|
|
|
|
|
|
+ * @param newCoordinates
|
|
|
*/
|
|
*/
|
|
|
-const onChange = ({ coordinates: config } : any) => {
|
|
|
|
|
- coordinates.value = config;
|
|
|
|
|
|
|
+const onCropperChange = ({ coordinates: newCoordinates }: any) => {
|
|
|
|
|
+ cropperConfig.value = newCoordinates
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Lorsque l'on sauvegarde l'image
|
|
|
|
|
|
|
+ * Annule l'upload et les modifications, et ferme la modale
|
|
|
*/
|
|
*/
|
|
|
-// TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
|
|
|
|
|
-const save = async () => {
|
|
|
|
|
- file.config = JSON.stringify({
|
|
|
|
|
- x: coordinates.value.left,
|
|
|
|
|
- y: coordinates.value.top,
|
|
|
|
|
- height: coordinates.value.height,
|
|
|
|
|
- width: coordinates.value.width
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- if (image.value.id > 0) {
|
|
|
|
|
- // Mise à jour d'une image existante : on bouge simplement le cropper
|
|
|
|
|
-
|
|
|
|
|
- file.id = image.value.id as number
|
|
|
|
|
-
|
|
|
|
|
- await em.persist(File, file) // TODO: à revoir
|
|
|
|
|
|
|
+const cancel = () => {
|
|
|
|
|
+ reset()
|
|
|
|
|
+ showModal.value = false
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // On émet un évent afin de mettre à jour le formulaire de départ
|
|
|
|
|
- emit('reload')
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Enregistre une nouvelle image et retourne l'id du fichier nouvellement créé
|
|
|
|
|
+ */
|
|
|
|
|
+const saveNewImage = async (): Promise<number> => {
|
|
|
|
|
+ if (!file.value) {
|
|
|
|
|
+ throw new Error('No File object defined')
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!currentImage.value.name) {
|
|
|
|
|
+ throw new Error("Missing file's name")
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!currentImage.value.content) {
|
|
|
|
|
+ throw new Error("Missing file's content")
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- } else {
|
|
|
|
|
- // Création d'une nouvelle image
|
|
|
|
|
- if (image.value.file) {
|
|
|
|
|
|
|
+ const config = JSON.stringify({
|
|
|
|
|
+ x: cropperConfig.value.left,
|
|
|
|
|
+ y: cropperConfig.value.top,
|
|
|
|
|
+ height: cropperConfig.value.height,
|
|
|
|
|
+ width: cropperConfig.value.width,
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- // On créé l'objet File à sauvegarder
|
|
|
|
|
- file.name = image.value.name
|
|
|
|
|
- file.imgFieldName = props.field
|
|
|
|
|
- file.visibility = 'EVERYBODY'
|
|
|
|
|
- file.folder = 'IMAGES'
|
|
|
|
|
- file.status = 'READY'
|
|
|
|
|
|
|
+ const response = (await imageManager.upload(
|
|
|
|
|
+ currentImage.value.name,
|
|
|
|
|
+ currentImage.value.content,
|
|
|
|
|
+ FILE_VISIBILITY.EVERYBODY,
|
|
|
|
|
+ config,
|
|
|
|
|
+ )) as any
|
|
|
|
|
|
|
|
- if (props.ownerId) {
|
|
|
|
|
- file.ownerId = props.ownerId
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return response.fileId
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const returnedFile = await em.persist(File, file) // TODO: à revoir, il faudra pouvoir passer `image.value.file` avec la requête
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Met à jour l'image existante
|
|
|
|
|
+ */
|
|
|
|
|
+const saveExistingImage = async () => {
|
|
|
|
|
+ if (!file.value) {
|
|
|
|
|
+ throw new Error('No File object defined')
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- //On émet un évent afin de mettre à jour le formulaire de départ
|
|
|
|
|
- emit('update', returnedFile.data['@id'])
|
|
|
|
|
|
|
+ file.value.config = JSON.stringify({
|
|
|
|
|
+ x: cropperConfig.value.left,
|
|
|
|
|
+ y: cropperConfig.value.top,
|
|
|
|
|
+ height: cropperConfig.value.height,
|
|
|
|
|
+ width: cropperConfig.value.width,
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- } else {
|
|
|
|
|
- // On reset l'image : on a appuyé sur "poubelle" puis on enregistre
|
|
|
|
|
- emit('reset')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ await em.persist(File, file.value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * On choisit de supprimer l'image présente
|
|
|
|
|
|
|
+ * Lorsque l'on sauvegarde l'image
|
|
|
*/
|
|
*/
|
|
|
-const reset = () => {
|
|
|
|
|
- image.value.src = null
|
|
|
|
|
- image.value.file = null
|
|
|
|
|
- image.value.name = null
|
|
|
|
|
- image.value.id = null
|
|
|
|
|
- URL.revokeObjectURL(image.value.src)
|
|
|
|
|
|
|
+// TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
|
|
|
|
|
+const save = async () => {
|
|
|
|
|
+ pageStore.loading = true
|
|
|
|
|
+
|
|
|
|
|
+ if (currentImage.value.src && currentImage.value.id === null) {
|
|
|
|
|
+ // Une nouvelle image a été uploadée
|
|
|
|
|
+ const fileId = await saveNewImage()
|
|
|
|
|
+ emit('update:modelValue', fileId)
|
|
|
|
|
+ } else if (currentImage.value.id) {
|
|
|
|
|
+ // L'image existante a été modifiée
|
|
|
|
|
+ await saveExistingImage()
|
|
|
|
|
+ uiImage.value.refresh()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // On a reset l'image
|
|
|
|
|
+ emit('update:modelValue', null)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ showModal.value = false
|
|
|
|
|
+ pageStore.loading = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
|
|
* Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
|
|
|
*/
|
|
*/
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
- unwatch()
|
|
|
|
|
- if (image.value.src) {
|
|
|
|
|
- URL.revokeObjectURL(image.value.src)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (currentImage.value && currentImage.value.src) {
|
|
|
|
|
+ URL.revokeObjectURL(currentImage.value.src)
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
-<style lang="scss">
|
|
|
|
|
- .vue-advanced-cropper__stretcher{
|
|
|
|
|
- height: auto !important;
|
|
|
|
|
- width: auto !important;
|
|
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+:deep(.vue-advanced-cropper__stretcher) {
|
|
|
|
|
+ height: auto !important;
|
|
|
|
|
+ width: auto !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading {
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload {
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ &__cropper {
|
|
|
|
|
+ border: solid 1px rgb(var(--v-theme-on-neutral-strong));
|
|
|
|
|
+ min-height: 300px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &__cropper-wrapper {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+ &__reset-button {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 20px;
|
|
|
|
|
+ bottom: 20px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 42px;
|
|
|
|
|
+ width: 42px;
|
|
|
|
|
+ background: rgb(var(--v-theme-neutral));
|
|
|
|
|
+ transition: background 0.5s;
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgb(var(--v-theme-primary-alt));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- .loading{
|
|
|
|
|
- height: 300px;
|
|
|
|
|
|
|
+ &__buttons-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-top: 17px;
|
|
|
}
|
|
}
|
|
|
- .upload {
|
|
|
|
|
- user-select: none;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- display: block;
|
|
|
|
|
- &__cropper {
|
|
|
|
|
- border: solid 1px rgb(var(--v-theme-on-neutral-strong));;
|
|
|
|
|
- min-height: 500px;
|
|
|
|
|
- max-height: 500px;
|
|
|
|
|
- }
|
|
|
|
|
- &__cropper-wrapper {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- }
|
|
|
|
|
- &__reset-button {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- right: 20px;
|
|
|
|
|
- bottom: 20px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- height: 42px;
|
|
|
|
|
- width: 42px;
|
|
|
|
|
- background: rgb(var(--v-theme-neutral));
|
|
|
|
|
- transition: background 0.5s;
|
|
|
|
|
- &:hover {
|
|
|
|
|
- background: rgb(var(--v-theme-primary-alt));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ &__button {
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ outline: solid transparent;
|
|
|
|
|
+ color: rgb(var(--v-theme-on-neutral));
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+ background: rgb(var(--v-theme-neutral));
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: background 0.5s;
|
|
|
|
|
+ margin: 0 16px;
|
|
|
|
|
+ &:hover,
|
|
|
|
|
+ &:focus {
|
|
|
|
|
+ background: rgb(var(--v-theme-primary-alt));
|
|
|
}
|
|
}
|
|
|
- &__buttons-wrapper {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- margin-top: 17px;
|
|
|
|
|
- }
|
|
|
|
|
- &__button {
|
|
|
|
|
- border: none;
|
|
|
|
|
- outline: solid transparent;
|
|
|
|
|
- color: rgb(var(--v-theme-on-neutral));
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- padding: 10px 20px;
|
|
|
|
|
- background: rgb(var(--v-theme-neutral));
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition: background 0.5s;
|
|
|
|
|
- margin: 0 16px;
|
|
|
|
|
- &:hover,
|
|
|
|
|
- &:focus {
|
|
|
|
|
- background: rgb(var(--v-theme-primary-alt));
|
|
|
|
|
- }
|
|
|
|
|
- input {
|
|
|
|
|
- display: none;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ input {
|
|
|
|
|
+ display: none;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.max-size-label {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: rgb(var(--v-theme-on-neutral-soft));
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|