gallery support

This commit is contained in:
2023-07-18 18:35:58 +10:00
parent 64ef8e9acd
commit ef4efe723f
18 changed files with 447 additions and 119 deletions

View File

@@ -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">&times;</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>

View File

@@ -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>

View File

@@ -96,6 +96,7 @@ export interface Article {
content: string;
publish_at: string;
hero: Media;
gallery: Array<Media>;
attachments: Array<Media>;
}

View File

@@ -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: [],
});
/**

View File

@@ -137,7 +137,7 @@ watch(
() => articlesPage.value,
() => {
handleLoad();
}
},
);
handleLoad();

View File

@@ -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",
);
}
}