aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2025-11-06 11:37:58 +0100
committerMurtaza Sultani <sultani@data-quest.de>2025-11-06 11:37:58 +0100
commit47768ce55e420f4aef1b6692c9ef71ebbd89b38c (patch)
tree762757db81c4b6297cacfa6affd47988cbf09089 /resources
parent51d548553e11e77f08bdd7a7700fe1ce4fbd0f79 (diff)
Resolve "Timeline in Forum"
Closes #5911 Merge request studip/studip!4517
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/stylesheets/scss/forum.scss133
-rw-r--r--resources/vue/apps/forum/discussions/Show.vue45
-rw-r--r--resources/vue/components/forum/discussions/DiscussionTimeline.vue298
-rw-r--r--resources/vue/components/forum/posts/Post.vue76
-rw-r--r--resources/vue/store/pinia/forum/ForumPost.js9
5 files changed, 415 insertions, 146 deletions
diff --git a/resources/assets/stylesheets/scss/forum.scss b/resources/assets/stylesheets/scss/forum.scss
index 480a3ca..2ffb2f9 100644
--- a/resources/assets/stylesheets/scss/forum.scss
+++ b/resources/assets/stylesheets/scss/forum.scss
@@ -693,6 +693,95 @@ $card-max-width: 300px;
}
}
+ .discussion-timeline {
+ position: sticky;
+ top: 50px;
+
+ &__start,
+ &__end {
+ button {
+ font-weight: bold;
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ }
+ }
+
+ .scroll-area {
+ height: 300px;
+ display: flex;
+ gap: 15px;
+ position: relative;
+ cursor: pointer;
+ margin: 10px 5px;
+
+ &__track {
+ position: relative;
+ background: var(--dark-gray-color-20);
+ width: 2px;
+ height: inherit;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+
+ &__unread {
+ position: absolute;
+ background-color: var(--red);
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ &__new-from {
+ position: absolute;
+ width: 100%;
+ display: flex;
+ align-items: end;
+
+ button {
+ background: none;
+ border: none;
+ padding: 0;
+ width: 100%;
+ text-align: left;
+ transition: opacity 5s;
+ cursor: pointer;
+ font-weight: bold;
+ margin: 0 10px;
+ }
+ }
+
+ &__scroller {
+ background: white;
+ border: none;
+ padding: 0;
+ position: absolute;
+ width: 100%;
+ height: 50px;
+ cursor: ns-resize;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ &__scroll-marker {
+ border-radius: 20px;
+ background: var(--color--brand-primary);
+ width: 6px;
+ height: inherit;
+ margin-left: -2px;
+ }
+
+ &__info {
+ font-weight: bold;
+ text-align: left;
+ flex: 1;
+ color: var(--color--brand-primary);
+ }
+ }
+ }
+
.posts-container {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
@@ -956,46 +1045,6 @@ $card-max-width: 300px;
}
}
- .timeline-container {
- position: sticky;
- top: 50px;
- }
-
- .discussion-timeline-table {
- width: 100%;
- height: 350px;
-
- tr td:first-child {
- width: 6px;
- background-color: var(--color--tile-title-background);
- }
-
- tr:first-child td:first-child {
- background-color: var(--color--highlight);
- }
-
- tr td:nth-child(2) {
- padding: 0 10px;
- }
-
- time, p {
- color: var(--color--font-secondary);
- margin: 0;
- }
-
- time {
- font-weight: 600;
- }
-
- p {
- font-size: small;
- }
-
- td.bg-new-activity {
- background-color: $red !important;
- }
- }
-
.drag-handle {
display: inline-block;
width: 6px;
@@ -1581,7 +1630,7 @@ $card-max-width: 300px;
grid-template-columns: 1fr;
}
- .timeline-container {
+ .discussion-timeline {
display: none;
}
@@ -1724,7 +1773,7 @@ $card-max-width: 300px;
}
.fullscreen-mode .forum {
- .timeline-container {
+ .discussion-timeline {
top: 120px;
}
diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue
index 08edb6b..4fa4fad 100644
--- a/resources/vue/apps/forum/discussions/Show.vue
+++ b/resources/vue/apps/forum/discussions/Show.vue
@@ -1,19 +1,20 @@
<script setup>
import {onMounted, computed, ref} from "vue";
import ForumApp from "@/vue/components/forum/ForumApp.vue";
-import {useForumPost} from "../../../store/pinia/forum/ForumPost";
-import {$gettext} from "../../../../assets/javascripts/lib/gettext";
+import {useForumPost} from "@/vue/store/pinia/forum/ForumPost";
+import {$gettext} from "@/assets/javascripts/lib/gettext";
import Post from "@/vue/components/forum/posts/Post.vue";
import PostCreateForm from "@/vue/components/forum/posts/PostCreateForm.vue";
import Loader from "@/vue/components/forum/Loader.vue";
-import {useForumConfig} from "../../../store/pinia/forum/ForumConfig";
-import StudipIcon from "../../../components/StudipIcon.vue";
-import StudipDateTime from "../../../components/StudipDateTime.vue";
+import {useForumConfig} from "@/vue/store/pinia/forum/ForumConfig";
+import StudipIcon from "@/vue/components/StudipIcon.vue";
+import StudipDateTime from "@/vue/components/StudipDateTime.vue";
import SubscriptionDropdown from "@/vue/components/forum/SubscriptionDropdown.vue";
import {highlightText, removeHighlight} from "@/vue/components/forum/helpers";
import {getSearchURL, getTopicURL, getDiscussionIndexURL} from "@/vue/components/forum/helpers/urls";
-import {deserializeJSONAPIResponse} from "../../../../assets/javascripts/lib/jsonapiUtils";
-import DiscussionFooter from "../../../components/forum/discussions/DiscussionFooter.vue";
+import {deserializeJSONAPIResponse} from "@/assets/javascripts/lib/jsonapiUtils";
+import DiscussionFooter from "@/vue/components/forum/discussions/DiscussionFooter.vue";
+import DiscussionTimeline from "@/vue/components/forum/discussions/DiscussionTimeline.vue";
const forumConfig = useForumConfig();
const forumPostStore = useForumPost();
@@ -114,6 +115,14 @@ const fetchPostings = async () => {
}
};
+const jumpTo = targetElement => {
+ if (!targetElement) {
+ return;
+ }
+
+ targetElement.scrollIntoView({ behavior: 'instant' });
+ requestAnimationFrame(() => STUDIP.eventBus.emit('forum:jumpToPost', targetElement.dataset?.index || 0));
+};
onMounted(async () => {
await fetchPostings();
@@ -123,18 +132,22 @@ onMounted(async () => {
if (urlHash === 'new-post') {
postCreateForm.value = true;
}
- document.getElementById(urlHash)?.scrollIntoView();
+ jumpTo(document.getElementById(urlHash))
} else if (props.read_index < posts.value.length) {
- document.querySelectorAll(".post")[props.read_index].scrollIntoView();
+ if (props.read_index === 0) {
+ jumpTo(document.getElementById('discussion_start'));
+ } else {
+ jumpTo(document.querySelector(`[data-index='${props.read_index}']`));
+ }
}
if (props.search_keyword !== "") {
highlightText(props.search_keyword, '.post-content');
- document.querySelector('.post-content mark')?.scrollIntoView();
+ jumpTo(document.querySelector('.post-content mark'))
// remove highlights
- document.getElementById("discussion_start").addEventListener("click", function() {
+ document.getElementById('discussion_start').addEventListener('click', function() {
removeHighlight('.post-content mark');
});
}
@@ -207,7 +220,7 @@ onMounted(async () => {
</header>
<div class="discussion">
<template v-if="posts[0]">
- <Post :post="posts[0]" :auth_user="auth_user" :discussion="discussion" :is_unread="read_index === 0" />
+ <Post :post="posts[0]" :auth_user="auth_user" :discussion="discussion" :readIndex="read_index" />
</template>
<div v-else class="discussion__body">
<Loader v-if="isLoading" />
@@ -230,7 +243,8 @@ onMounted(async () => {
:post="post"
:auth_user="auth_user"
:discussion="discussion"
- :is_unread="read_index < index + 2"
+ :index="index + 1"
+ :readIndex="read_index"
/>
<hr v-if="index < posts.length - 2" class="divider m-0" />
</template>
@@ -253,6 +267,10 @@ onMounted(async () => {
@created="addPost"
/>
</div>
+
+ <template #sidebar>
+ <DiscussionTimeline :discussion="discussion" :posts="posts" :readIndex="read_index" />
+ </template>
</ForumApp>
</template>
@@ -267,5 +285,6 @@ onMounted(async () => {
}
html {
scroll-behavior: smooth;
+ scroll-padding-top: 50px !important;
}
</style>
diff --git a/resources/vue/components/forum/discussions/DiscussionTimeline.vue b/resources/vue/components/forum/discussions/DiscussionTimeline.vue
index c394d01..f54aa61 100644
--- a/resources/vue/components/forum/discussions/DiscussionTimeline.vue
+++ b/resources/vue/components/forum/discussions/DiscussionTimeline.vue
@@ -1,88 +1,252 @@
<script setup>
-import {computed} from "vue";
-import StudipDateTime from "@/vue/components/StudipDateTime.vue";
+import {computed, onMounted, onUnmounted, ref} from 'vue';
+import StudipDateTime from '@/vue/components/StudipDateTime.vue';
+import {useForumPost} from '@/vue/store/pinia/forum/ForumPost';
+import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig';
+import {$gettext} from '@/assets/javascripts/lib/gettext';
+const forumConfig = useForumConfig();
+const forumPostStore = useForumPost();
const props = defineProps({
+ discussion: {
+ type: Object,
+ required: true,
+ },
posts: {
type: Array,
required: true,
},
- read_index: {
+ readIndex: {
type: Number,
- required: true,
default: 0
- },
- discussion: {
- type: Object,
- required: true,
}
});
-const readPosts = computed(() => props.posts.slice(0, props.read_index));
+const scrollerTop = ref(0);
+const isDragging = ref(false);
+const unreadScrollPosition = ref(-1);
+
+const othersPosts = computed(() => props.posts.filter(({ author }) => author?.id !== STUDIP.USER_ID));
+const currentPostIndex = computed(() => forumPostStore.currentPostIndex);
+const currentPostDate = computed(() => {
+ if (currentPostIndex.value < props.posts.length) {
+ const date = new Date(props.posts[currentPostIndex.value].mkdate);
+ return date.toLocaleString(String.locale, { month: 'long', year: 'numeric' });
+ }
+
+ return null;
+});
+const isNewFrom = computed(() => {
+ return unreadScrollPosition.value >= 0
+ && props.readIndex < othersPosts.value.length
+ && !forumConfig.allowGuestAccess
+});
+
+const findPostAtScroll = y => {
+ const postElements = document.querySelectorAll('.post');
+ for (const postElement of postElements) {
+ const postScrollPosition = postElement.getBoundingClientRect().top + window.scrollY;
+
+ if (postScrollPosition > y) {
+ return postElement;
+ }
+ }
-const unreadPosts = computed(() => props.posts.slice(props.read_index));
+ return null;
+}
+
+const jumpToPost = (targetPost, index = 0) => {
+ if (!targetPost) {
+ targetPost = document.querySelector(`[data-index='${index}']`);
+ }
-const readPostsPercentage = computed(() => {
- if (props.posts.length === 0) {
- return 100;
+ if (parseInt(targetPost?.dataset.index) === 0) {
+ document.getElementById('discussion_start').scrollIntoView({ behavior: 'instant' });
+ return;
}
- return parseFloat((props.posts.length - unreadPosts.value.length) * 100 / props.posts.length);
+ targetPost?.scrollIntoView({ behavior: 'instant' });
+}
+
+const jumpTo = e => {
+ const contentContainer = document.documentElement;
+ const trackRect = e.currentTarget.getBoundingClientRect();
+ const clickY = e.clientY - trackRect.top;
+ const percent = Math.min(Math.max(clickY / trackRect.height, 0), 1);
+
+ const scrollPosition = percent * (contentContainer.scrollHeight - contentContainer.clientHeight);
+ const targetPost = findPostAtScroll(scrollPosition);
+
+ if (targetPost) {
+ jumpToPost(targetPost);
+ updateScroller(scrollPosition);
+ } else {
+ contentContainer.scrollTop = scrollPosition;
+ }
+}
+
+const startDrag = e => {
+ isDragging.value = true;
+ let scrollPosition = 0;
+ let targetPost = null;
+
+ const contentContainer = document.documentElement;
+ const rectScrollArea = document.getElementById('scroll-area').getBoundingClientRect();
+ const scrollerRect = document.getElementById('scroller').getBoundingClientRect();
+
+ const offsetY = e.clientY - (scrollerRect.top + scrollerRect.height / 2);
+
+ const onDrag = e2 => {
+ const y = e2.clientY - rectScrollArea.top - offsetY;
+ const percent = Math.min(Math.max(y / rectScrollArea.height, 0), 1);
+
+ scrollerTop.value = percent * 100;
+ scrollPosition = percent * (contentContainer.scrollHeight - contentContainer.clientHeight);
+ targetPost = findPostAtScroll(scrollPosition);
+ forumPostStore.updateCurrentPostIndex(parseInt(targetPost?.dataset.index ?? 0))
+ updateScroller(scrollPosition);
+ };
+
+ const onDrop = () => {
+ if (targetPost) {
+ jumpToPost(targetPost);
+ } else {
+ contentContainer.scrollTop = scrollPosition;
+ }
+
+ isDragging.value = false;
+
+ document.removeEventListener('mousemove', onDrag);
+ document.removeEventListener('mouseup', onDrop);
+ };
+
+ document.addEventListener('mousemove', onDrag);
+ document.addEventListener('mouseup', onDrop);
+}
+
+const updateScroller = (scrollPosition = -1, ignoreOffset = 0) => {
+ const contentContainer = document.documentElement;
+ scrollPosition = scrollPosition > -1 ? scrollPosition : contentContainer.scrollTop;
+ const range = Math.max(1, contentContainer.scrollHeight - contentContainer.clientHeight - ignoreOffset);
+ scrollerTop.value = Math.min(100, Math.max(0, scrollPosition - ignoreOffset) / range * 100);
+
+ if (scrollerTop.value === 0) {
+ forumPostStore.updateCurrentPostIndex(0);
+ }
+}
+
+const handleScroll = () => {
+ if (!isDragging.value) {
+ updateScroller(window.scrollY, 200);
+ }
+};
+
+const updateUnreadScrollPosition = () => {
+ if (props.readIndex === 0) {
+ unreadScrollPosition.value = 0;
+ return;
+ }
+
+ const firstUnreadPost = document.querySelector(`[data-index='${props.readIndex}']`);
+ if (!firstUnreadPost) {
+ return;
+ }
+
+ const contentContainer = document.documentElement;
+ const elementTop = firstUnreadPost.getBoundingClientRect().top + window.scrollY - 200;
+ const scrollableHeight = contentContainer.scrollHeight - contentContainer.clientHeight;
+ unreadScrollPosition.value = Math.min(Math.max((elementTop / scrollableHeight) * 100, 0), 90);
+};
+
+onMounted(() => {
+ window.addEventListener('scroll', handleScroll);
+ STUDIP.eventBus.on('forum:jumpToPost', updateUnreadScrollPosition);
+});
+
+onUnmounted(() => {
+ window.removeEventListener('scroll', handleScroll);
+ STUDIP.eventBus.off('forum:jumpToPost', updateUnreadScrollPosition);
});
</script>
<template>
- <table class="discussion-timeline-table" cellspacing="0">
- <tbody>
- <tr>
- <td></td>
- <td>
- <a href="#discussion_start">
- <StudipDateTime :iso="discussion.mkdate" :relative="true" />
- <p>1/{{ posts.length }}</p>
- </a>
- </td>
- </tr>
- <tr v-if="readPostsPercentage > 0" :style="{height: readPostsPercentage+'%' }">
- <td></td>
- <td></td>
- </tr>
- <template v-if="unreadPosts.length > 0">
- <tr>
- <td class="bg-new-activity"></td>
- <td>
- <a :href="'#post_'+unreadPosts[0].id">
- <StudipDateTime :iso="unreadPosts[0].mkdate" :relative="true" />
- <p>{{ readPosts.length + 1 }}/{{ posts.length }} - {{ $gettext('neu ab hier') }}</p>
- </a>
- </td>
- </tr>
- <tr :style="{height: (100 - readPostsPercentage)+'%' }">
- <td class="bg-new-activity"></td>
- <td></td>
- </tr>
- </template>
- <tr v-if="posts.length > 0">
- <td class="bg-new-activity"></td>
- <td>
- <a :href="'#post_'+posts[posts.length -1].id">
- <StudipDateTime :iso="posts[posts.length -1].mkdate" :relative="true" />
- <p>{{ posts.length }}/{{ posts.length }}</p>
- </a>
- </td>
- </tr>
- <tr v-else>
- <td></td>
- <td>
- <StudipDateTime :iso="discussion.mkdate" :relative="true" />
- <p>{{ $gettext('Keine Beitrag bis hier') }}</p>
- </td>
- </tr>
- </tbody>
- </table>
+ <div class="discussion-timeline">
+ <div class="discussion-timeline__start">
+ <button
+ type="button"
+ class="button-base"
+ @click="jumpToPost(null, 0)"
+ :title="$gettext('Zum ersten Beitrag')"
+ >
+ <StudipDateTime :iso="discussion.mkdate" :relative="true" />
+ </button>
+ </div>
+ <div id="scroll-area" class="scroll-area" @click="jumpTo">
+ <div class="scroll-area__track">
+ <Transition name="fade">
+ <div
+ v-if="isNewFrom"
+ class="scroll-area__unread"
+ :style="{
+ top: `${unreadScrollPosition}%`
+ }"
+ >
+ </div>
+ </Transition>
+ </div>
+ <Transition name="fade">
+ <div
+ v-if="isNewFrom && currentPostIndex !== readIndex"
+ class="scroll-area__new-from"
+ :style="{
+ top: `${unreadScrollPosition}%`
+ }">
+ <button
+ type="button"
+ class="button-base"
+ @click.stop="jumpToPost(null, readIndex)"
+ :title="$gettext('Zum ersten ungelesenen Beitrag')"
+ >
+ {{ $gettext('Neu ab hier') }}
+ </button>
+ </div>
+ </Transition>
+ <button
+ type="button"
+ id="scroller"
+ class="scroll-area__scroller"
+ :style="{
+ top: `${scrollerTop}%`,
+ transform: `translateY(-${scrollerTop}%)`,
+ cursor: posts.length > 1 ? 'ns-resize' : 'not-allowed'
+ }"
+ @mousedown.prevent="startDrag"
+ @click.stop
+ >
+ <span class="scroll-area__scroll-marker"></span>
+ <span class="scroll-area__info">
+ {{ currentPostIndex + 1 }}/{{ posts.length }} <br />
+ <time v-if="currentPostDate" :datetime="currentPostDate">
+ {{ currentPostDate }}
+ </time>
+ <Transition name="fade">
+ <span v-if="isNewFrom && currentPostIndex === readIndex">
+ &mdash; {{ $gettext('Neu ab hier') }}
+ </span>
+ </Transition>
+ </span>
+ </button>
+ </div>
+ <div class="discussion-timeline__end">
+ <button
+ type="button"
+ class="button-base"
+ @click="jumpToPost(null, posts.length -1)"
+ :title="$gettext('Zum letzten Beitrag')"
+ >
+ <StudipDateTime v-if="posts.length > 0" :iso="posts[posts.length -1].mkdate" :relative="true" />
+ <StudipDateTime v-else :iso="discussion.mkdate" :relative="true" />
+ </button>
+ </div>
+ </div>
</template>
-<style>
-html {
- scroll-behavior: smooth;
-}
-</style>
diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue
index ff935e7..1f8a5a7 100644
--- a/resources/vue/components/forum/posts/Post.vue
+++ b/resources/vue/components/forum/posts/Post.vue
@@ -1,21 +1,21 @@
<script setup>
-import {computed, ref, useTemplateRef} from "vue";
-import PostEditForm from "./PostEditForm.vue";
-import PostCreateForm from "./PostCreateForm.vue";
-import PostContent from "@/vue/components/forum/posts/PostContent.vue";
-import UserAvatarDropdown from "@/vue/components/avatar/UserAvatarDropdown.vue";
-import PostReactions from "./PostReactions.vue";
-import {useForumPost} from "../../../store/pinia/forum/ForumPost";
-import {getDiscussionURL} from "@/vue/components/forum/helpers/urls";
-import StudipDateTime from "@/vue/components/StudipDateTime.vue";
-import StudipIcon from "@/vue/components/StudipIcon.vue";
-import {$gettext} from "@/assets/javascripts/lib/gettext";
-import LinksPreview from "@/vue/components/LinksPreview.vue";
-import {userProfileURL} from "../helpers/urls";
-import {useForumConfig} from "../../../store/pinia/forum/ForumConfig";
+import {computed, onBeforeUnmount, onMounted, ref, useTemplateRef} from 'vue';
+import PostEditForm from './PostEditForm.vue';
+import PostCreateForm from './PostCreateForm.vue';
+import PostContent from '@/vue/components/forum/posts/PostContent.vue';
+import PostReactions from './PostReactions.vue';
+import {getDiscussionURL} from '@/vue/components/forum/helpers/urls';
+import StudipDateTime from '@/vue/components/StudipDateTime.vue';
+import StudipIcon from '@/vue/components/StudipIcon.vue';
+import {$gettext} from '@/assets/javascripts/lib/gettext';
+import LinksPreview from '@/vue/components/LinksPreview.vue';
+import UserAvatarDropdown from '@/vue/components/avatar/UserAvatarDropdown.vue';
+import {userProfileURL} from '../helpers/urls';
+import {useForumPost} from '@/vue/store/pinia/forum/ForumPost';
+import {useForumConfig} from '@/vue/store/pinia/forum/ForumConfig';
const forumConfig = useForumConfig();
-const forumDiscussionPost = useForumPost();
+const forumPostStore = useForumPost();
const props = defineProps({
discussion: {
type: Object,
@@ -29,12 +29,17 @@ const props = defineProps({
type: Object,
required: true
},
- is_unread: {
- type: Boolean,
- default: false
+ index: {
+ type: Number,
+ default: 0
+ },
+ readIndex: {
+ type: Number,
+ default: 0
}
});
+const postRef = useTemplateRef('postRef');
const postContentRef = useTemplateRef('postContent');
const userAvatarContainerRef = useTemplateRef('userAvatarContainer');
@@ -42,7 +47,7 @@ const selectedText = ref('');
const showPostEditForm = ref(false);
const showPostCreateForm = ref(false);
-const isUnread = computed(() => (!props.post.author && props.is_unread) || (props.is_unread && props.post.author.id !== STUDIP.USER_ID))
+const isUnread = computed(() => (!props.post.author && props.index >= props.readIndex) || (props.index >= props.readIndex && props.post.author.id !== STUDIP.USER_ID))
const canEditPost = computed(() => forumConfig.isTutor || (props.post.author?.id === STUDIP.USER_ID && !props.discussion.closed_at));
const canDeletePost = computed(() => canEditPost.value);
const copyToClipboard = () => {
@@ -71,13 +76,14 @@ const deletePost = async () => {
async () => {
try {
await STUDIP.jsonapi.withPromises().DELETE(`forum-postings/${props.post.id}`);
- forumDiscussionPost.removePost(props.post.id);
+ forumPostStore.removePost(props.post.id);
STUDIP.Report.success($gettext('Der Beitrag wurde gelöscht.'));
} catch (error) {
STUDIP.Report.error(error);
}
},
- STUDIP.Dialog.close());
+ STUDIP.Dialog.close()
+ );
}
const addPost = () => {
@@ -113,15 +119,39 @@ const forwardPost = post => {
const removePostHighlight = id => {
const element = document.getElementById(id);
if (!element) {
- console.error("Element not found!");
+ console.error('Element not found!');
return;
}
element.classList.remove('--highlight');
}
+
+let postObserver = null;
+
+onMounted(() => {
+ postObserver = new IntersectionObserver(
+ entries => entries.forEach(e => {
+ if (e.isIntersecting) {
+ forumPostStore.updateCurrentPostIndex(props.index);
+ }
+ }),{
+ rootMargin: `-110px 0px -${document.documentElement.clientHeight - 120}px 0px`
+ }
+ );
+
+ postObserver.observe(postRef.value);
+});
+
+onBeforeUnmount(() => postObserver.disconnect());
</script>
<template>
- <div :id="`post_${post.id}`" class="post" @click="removePostHighlight(`post_${post.id}`)">
+ <div
+ ref="postRef"
+ :id="`post_${post.id}`"
+ class="post"
+ :data-index="index"
+ @click="removePostHighlight(`post_${post.id}`)"
+ >
<div v-if="!forumConfig.allowGuestAccess && isUnread" class="post__unread">
</div>
<div class="post__body">
diff --git a/resources/vue/store/pinia/forum/ForumPost.js b/resources/vue/store/pinia/forum/ForumPost.js
index 3990ccc..a5fa749 100644
--- a/resources/vue/store/pinia/forum/ForumPost.js
+++ b/resources/vue/store/pinia/forum/ForumPost.js
@@ -5,6 +5,7 @@ export const useForumPost = defineStore(
() => {
const posts = ref([]);
+ const currentPostIndex = ref(0);
function initPosts(newPosts) {
posts.value = newPosts;
@@ -40,14 +41,20 @@ export const useForumPost = defineStore(
}
}
+ function updateCurrentPostIndex(index) {
+ currentPostIndex.value = index;
+ }
+
return {
posts,
+ currentPostIndex,
initPosts,
addPost,
updatePost,
removePost,
addPostReaction,
- removePostReaction
+ removePostReaction,
+ updateCurrentPostIndex
}
}
)