gallery support
This commit is contained in:
@@ -1,17 +1,53 @@
|
||||
<template>
|
||||
<div class="image-gallery" ref="gallery">
|
||||
<div
|
||||
:class="[
|
||||
'flex',
|
||||
'gap-4',
|
||||
'my-4',
|
||||
'select-none',
|
||||
props.showEditor
|
||||
? ['overflow-auto']
|
||||
: ['flex-wrap', 'flex-justify-center'],
|
||||
]">
|
||||
<div
|
||||
class="image-gallery-item"
|
||||
v-for="(image, index) in images"
|
||||
v-for="(image, index) in modelValue"
|
||||
class="flex flex-col flex-justify-center relative sm-gallery-item p-1"
|
||||
:key="index">
|
||||
<img
|
||||
:src="image as string"
|
||||
class="image-gallery-image"
|
||||
@click="showModal(index)" />
|
||||
:src="mediaGetVariantUrl(image as Media, 'small')"
|
||||
class="max-h-40 max-w-40 cursor-pointer"
|
||||
@click="showGalleryModal(index)" />
|
||||
<div
|
||||
class="absolute rounded-5 bg-white -top-0.25 -right-0.25 hidden cursor-pointer item-delete"
|
||||
@click="handleRemoveItem(image.id)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 block"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
|
||||
fill="rgba(185,28,28,1)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.showEditor" class="flex flex-col flex-justify-center">
|
||||
<div
|
||||
class="flex flex-col flex-justify-center flex-items-center h-23 w-40 cursor-pointer bg-gray-300 text-gray-800 hover:text-gray-600"
|
||||
@click="handleAddToGallery">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-15 w-15"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Add image</title>
|
||||
<path
|
||||
d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showModalImage !== null"
|
||||
v-if="props.showEditor == false && showModalImage !== null"
|
||||
:class="[
|
||||
'image-gallery-modal',
|
||||
{ 'image-gallery-modal-buttons': showButtons },
|
||||
@@ -20,7 +56,7 @@
|
||||
@mousemove="handleModalUpdateButtons"
|
||||
@mouseleave="handleModalUpdateButtons">
|
||||
<img
|
||||
:src="images[showModalImage] as string"
|
||||
:src="mediaGetVariantUrl(modelValue[showModalImage] as Media)"
|
||||
class="image-gallery-modal-image" />
|
||||
<div
|
||||
class="image-gallery-modal-prev"
|
||||
@@ -28,26 +64,46 @@
|
||||
<div
|
||||
class="image-gallery-modal-next"
|
||||
@click.stop="handleModalNextImage"></div>
|
||||
<div class="image-gallery-modal-close" @click="hideModal">×</div>
|
||||
<div class="image-gallery-modal-close" @click="hideModal">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Close</title>
|
||||
<path
|
||||
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
images: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
showEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const gallery = ref(null);
|
||||
const showModalImage = ref(null);
|
||||
let showButtons = ref(false);
|
||||
let mouseMoveTimeout = null;
|
||||
|
||||
const showModal = (index) => {
|
||||
const showGalleryModal = (index) => {
|
||||
showModalImage.value = index;
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
@@ -87,7 +143,7 @@ const handleModalPrevImage = () => {
|
||||
if (showModalImage.value > 0) {
|
||||
showModalImage.value--;
|
||||
} else {
|
||||
showModalImage.value = props.images.length - 1;
|
||||
showModalImage.value = props.modelValue.length - 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -96,7 +152,7 @@ const handleModalNextImage = () => {
|
||||
handleModalUpdateButtons();
|
||||
|
||||
if (showModalImage.value !== null) {
|
||||
if (showModalImage.value < props.images.length - 1) {
|
||||
if (showModalImage.value < props.modelValue.length - 1) {
|
||||
showModalImage.value++;
|
||||
} else {
|
||||
showModalImage.value = 0;
|
||||
@@ -104,6 +160,33 @@ const handleModalNextImage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToGallery = async () => {
|
||||
let result = await openDialog(SMDialogMedia, {
|
||||
allowUpload: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const mediaResult = result as Media[];
|
||||
let newValue = props.modelValue;
|
||||
let galleryIds = new Set(newValue.map((item) => item.id));
|
||||
|
||||
mediaResult.forEach((item) => {
|
||||
if (!galleryIds.has(item.id)) {
|
||||
newValue.push(item);
|
||||
galleryIds.add(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
emits("update:modelValue", newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveItem = async (id: string) => {
|
||||
const newList = props.modelValue.filter((item) => item.id !== id);
|
||||
emits("update:modelValue", newList);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
@@ -114,30 +197,30 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
// .image-gallery {
|
||||
// display: grid;
|
||||
// grid-template-columns: 1fr 1fr;
|
||||
// gap: 15px;
|
||||
|
||||
.image-gallery-image {
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
// .image-gallery-image {
|
||||
// cursor: pointer;
|
||||
// max-width: 100%;
|
||||
// max-height: 100%;
|
||||
// object-fit: contain;
|
||||
// }
|
||||
// }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.image-gallery {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
// @media (min-width: 768px) {
|
||||
// .image-gallery {
|
||||
// grid-template-columns: 1fr 1fr 1fr;
|
||||
// }
|
||||
// }
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.image-gallery {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
// @media (min-width: 1024px) {
|
||||
// .image-gallery {
|
||||
// grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
// }
|
||||
// }
|
||||
|
||||
.image-gallery-modal {
|
||||
position: fixed;
|
||||
@@ -268,4 +351,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sm-gallery-item:hover .item-delete {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -357,18 +357,6 @@
|
||||
"
|
||||
small
|
||||
class="bg-white bg-op-90 w-full h-full" />
|
||||
<div
|
||||
class="absolute border-1 border-1 rounded-5 bg-white -top-1.5 -right-1.5 hidden item-delete"
|
||||
@click="handleRemoveItem(item.id)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 block"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
|
||||
fill="rgba(185,28,28,1)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -96,6 +96,7 @@ export interface Article {
|
||||
content: string;
|
||||
publish_at: string;
|
||||
hero: Media;
|
||||
gallery: Array<Media>;
|
||||
attachments: Array<Media>;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,12 @@
|
||||
>
|
||||
</div>
|
||||
<SMHTML :html="article.content" />
|
||||
<SMAttachments :attachments="article.attachments || []" />
|
||||
<SMImageGallery
|
||||
v-if="article.gallery.length > 0"
|
||||
:model-value="article.gallery" />
|
||||
<SMAttachments
|
||||
v-if="article.attachments.length > 0"
|
||||
:attachments="article.attachments || []" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -54,6 +59,7 @@ import { userHasPermission } from "../helpers/utils";
|
||||
import SMLoading from "../components/SMLoading.vue";
|
||||
import SMPageStatus from "../components/SMPageStatus.vue";
|
||||
import SMHTML from "../components/SMHTML.vue";
|
||||
import SMImageGallery from "../components/SMImageGallery.vue";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
@@ -61,8 +67,18 @@ const applicationStore = useApplicationStore();
|
||||
* The article data.
|
||||
*/
|
||||
let article: Ref<Article> = ref({
|
||||
id: "",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
title: "",
|
||||
slug: "",
|
||||
user_id: "",
|
||||
user: { display_name: "" },
|
||||
content: "",
|
||||
publish_at: "",
|
||||
hero: {},
|
||||
gallery: [],
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,7 +137,7 @@ watch(
|
||||
() => articlesPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
class="mb-8"
|
||||
v-model:model-value="form.controls.content.value" />
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<h3>Gallery</h3>
|
||||
<p class="small">
|
||||
{{ gallery.length }} image{{
|
||||
gallery.length != 1 ? "s" : ""
|
||||
}}
|
||||
</p>
|
||||
<SMImageGallery show-editor v-model:model-value="gallery" />
|
||||
</div>
|
||||
<SMInputAttachments v-model:model-value="attachments" />
|
||||
<div class="flex flex-justify-end">
|
||||
<input
|
||||
@@ -70,6 +79,7 @@ import SMPageStatus from "../../components/SMPageStatus.vue";
|
||||
import { userHasPermission } from "../../helpers/utils";
|
||||
import SMSelectImage from "../../components/SMSelectImage.vue";
|
||||
import SMLoading from "../../components/SMLoading.vue";
|
||||
import SMImageGallery from "../../components/SMImageGallery.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -78,6 +88,7 @@ let pageError = ref(200);
|
||||
const authors = ref({});
|
||||
const attachments = ref([]);
|
||||
const pageHeading = route.params.id ? "Edit Article" : "Create Article";
|
||||
const gallery = ref([]);
|
||||
|
||||
const form = reactive(
|
||||
Form({
|
||||
@@ -85,12 +96,12 @@ const form = reactive(
|
||||
slug: FormControl("", And([Required(), Min(6)])),
|
||||
publish_at: FormControl(
|
||||
route.params.id ? "" : new SMDate("now").format("d/M/yy h:mm aa"),
|
||||
DateTime()
|
||||
DateTime(),
|
||||
),
|
||||
hero: FormControl(),
|
||||
user_id: FormControl(userStore.id),
|
||||
content: FormControl(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const updateSlug = async () => {
|
||||
@@ -164,8 +175,10 @@ const loadData = async () => {
|
||||
attachments.value = (data.article.attachments || []).map(
|
||||
function (attachment) {
|
||||
return attachment.id.toString();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
gallery.value = data.article.gallery;
|
||||
} else {
|
||||
pageError.value = 404;
|
||||
}
|
||||
@@ -183,11 +196,12 @@ const handleSubmit = async () => {
|
||||
title: form.controls.title.value,
|
||||
slug: form.controls.slug.value,
|
||||
publish_at: new SMDate(
|
||||
form.controls.publish_at.value as string
|
||||
form.controls.publish_at.value as string,
|
||||
).format("yyyy/MM/dd HH:mm:ss", { utc: true }),
|
||||
user_id: form.controls.user_id.value,
|
||||
content: form.controls.content.value,
|
||||
hero: form.controls.hero.value.id,
|
||||
gallery: gallery.value.map((item) => item.id),
|
||||
};
|
||||
|
||||
let article_id = "";
|
||||
@@ -280,7 +294,7 @@ const attachmentAdd = async (event) => {
|
||||
},
|
||||
progress: (progressEvent) =>
|
||||
event.attachment.setUploadProgress(
|
||||
(progressEvent.loaded * progressEvent.total) / 100
|
||||
(progressEvent.loaded * progressEvent.total) / 100,
|
||||
),
|
||||
});
|
||||
|
||||
@@ -292,7 +306,7 @@ const attachmentAdd = async (event) => {
|
||||
event.preventDefault();
|
||||
alert(
|
||||
err.response?.data?.message ||
|
||||
"An unexpected server error occurred"
|
||||
"An unexpected server error occurred",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user