From 88c4d415238ad48132c7a8288152f1c1b0d0d305 Mon Sep 17 00:00:00 2001 From: pallavibakale Date: Thu, 22 Jan 2026 16:03:22 -0500 Subject: [PATCH] fix: align floating comments by page - group floating comments per page using PresentationEditor bounds - normalize per-page positions and avoid overlaps in the sidebar - expand tracked-change and standard comment bodies when active - keep floating comments column full height for consistent layout --- packages/superdoc/src/SuperDoc.vue | 1 + .../CommentsLayer/CommentDialog.vue | 13 +- .../CommentsLayer/FloatingComments.vue | 195 ++++++++++++++---- 3 files changed, 163 insertions(+), 46 deletions(-) diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index 3c377a045a..152ab60a8a 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -1001,6 +1001,7 @@ watch(getFloatingComments, () => { .floating-comments { min-width: 300px; width: 300px; + height: 100%; } .superdoc__layers { diff --git a/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue b/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue index d0b5c47d37..0b995d6a40 100644 --- a/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue +++ b/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue @@ -380,7 +380,7 @@ onMounted(() => { @overflow-select="handleOverflowSelect($event, comment)" /> -
+
@@ -400,7 +400,7 @@ onMounted(() => {
-
+
{{ @@ -463,6 +463,15 @@ onMounted(() => { .initial-internal-dropdown { margin-top: 10px; } +.comment-body { + overflow-y: auto; + max-height: 4rem; + transition: max-height 250ms ease; +} +.comment-body.is-active { + max-height: none; + overflow-y: visible; +} .comments-dialog { display: flex; flex-direction: column; diff --git a/packages/superdoc/src/components/CommentsLayer/FloatingComments.vue b/packages/superdoc/src/components/CommentsLayer/FloatingComments.vue index 8b37b01faa..e1dbee4238 100644 --- a/packages/superdoc/src/components/CommentsLayer/FloatingComments.vue +++ b/packages/superdoc/src/components/CommentsLayer/FloatingComments.vue @@ -4,6 +4,7 @@ import { ref, computed, watchEffect, nextTick, watch, onMounted, onBeforeUnmount import { useCommentsStore } from '@superdoc/stores/comments-store'; import { useSuperdocStore } from '@superdoc/stores/superdoc-store'; import CommentDialog from '@superdoc/components/CommentsLayer/CommentDialog.vue'; +import { PresentationEditor } from '@superdoc/super-editor'; const props = defineProps({ currentDocument: { @@ -25,9 +26,10 @@ const { getFloatingComments, hasInitializedLocations, activeComment, commentsLis const floatingCommentsContainer = ref(null); const renderedSizes = ref([]); const firstGroupRendered = ref(false); -const verticalOffset = ref(0); const commentsRenderKey = ref(0); const measurementTimeoutId = ref(null); +const pageContainerRefs = ref({}); +const pagePositions = ref({}); const getCommentPosition = computed(() => (comment) => { if (!floatingCommentsContainer.value) return { top: '0px' }; @@ -37,6 +39,65 @@ const getCommentPosition = computed(() => (comment) => { return { top: `${comment.top}px` }; }); +// Group comments by page +const commentsByPage = computed(() => { + const grouped = {}; + + renderedSizes.value.forEach((comment) => { + const pageIndex = comment.pageIndex ?? 0; + if (!grouped[pageIndex]) { + grouped[pageIndex] = []; + } + grouped[pageIndex].push(comment); + }); + + return grouped; +}); + +// Calculate page container positions and heights +const calculatePagePositions = () => { + const presentation = PresentationEditor.getInstance(props.currentDocument.id); + if (!presentation) return {}; + + const pages = presentation.getPages(); + const positions = {}; + + pages.forEach((page, index) => { + // Page bounds give us the position and dimensions + // Each page container should align with its corresponding document page + const pageBounds = page.bounds || page; + positions[index] = { + top: pageBounds.y || index * 1056, // fallback: approximate page height with gap + height: pageBounds.h || pageBounds.height || 1056, + pageIndex: index, + }; + }); + + return positions; +}; + +// Get style for each page comment container +const getPageContainerStyle = computed(() => (pageIndex) => { + const position = pagePositions.value[pageIndex]; + const gapOffset = pageIndex * 24; + + if (!position) { + return { + position: 'absolute', + top: `${pageIndex * 1056 + gapOffset}px`, + height: '1056px', + width: '310px', + }; + } + + return { + position: 'absolute', + top: `${position.top + gapOffset}px`, + height: `${position.height}px`, + width: '310px', + }; +}); + const handleDialog = (dialog) => { if (!dialog) return; const { elementRef, commentId } = dialog; @@ -76,6 +137,9 @@ const handleDialog = (dialog) => { }; const processLocations = async () => { + // Calculate page positions first + pagePositions.value = calculatePagePositions(); + const groupedByPage = renderedSizes.value.reduce((acc, comment) => { const key = comment.pageIndex ?? 0; if (!acc[key]) acc[key] = []; @@ -83,17 +147,34 @@ const processLocations = async () => { return acc; }, {}); - Object.values(groupedByPage).forEach((comments) => { + // Process each page independently - positions are relative to page, not global + Object.entries(groupedByPage).forEach(([pageIndexStr, comments]) => { + const pageIndex = parseInt(pageIndexStr); + const pagePos = pagePositions.value[pageIndex]; + const pageTop = pagePos?.top || 0; + + // Normalize comment positions relative to the page + comments.forEach((comment) => { + // Comment top is absolute, make it relative to page + comment.relativeTop = comment.top - pageTop; + }); + + // Sort and adjust within page comments - .sort((a, b) => a.top - b.top) + .sort((a, b) => a.relativeTop - b.relativeTop) .forEach((comment, idx, arr) => { if (idx === 0) return; const prev = arr[idx - 1]; - const minTop = prev.top + prev.height + 15; - if (comment.top < minTop) { - comment.top = minTop; + const minTop = prev.relativeTop + prev.height + 15; + if (comment.relativeTop < minTop) { + comment.relativeTop = minTop; } }); + + // Update the actual top for rendering + comments.forEach((comment) => { + comment.top = comment.relativeTop; + }); }); await nextTick(); @@ -119,31 +200,6 @@ watchEffect(() => { nextTick(processLocations); }); -watch(activeComment, (newVal, oldVal) => { - nextTick(() => { - if (!activeComment.value) return (verticalOffset.value = 0); - - const comment = commentsStore.getComment(activeComment.value); - if (!comment) return (verticalOffset.value = 0); - const commentKey = comment.commentId || comment.importedId; - const renderedItem = renderedSizes.value.find((item) => item.id === commentKey); - if (!renderedItem) return (verticalOffset.value = 0); - - const selectionTop = comment.selection.selectionBounds.top; - const renderedTop = renderedItem.top; - - const editorBounds = floatingCommentsContainer.value.getBoundingClientRect(); - verticalOffset.value = selectionTop - renderedTop; - - setTimeout(() => { - renderedItem.elementRef.value?.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); - }, 200); - }); -}); - onBeforeUnmount(() => { // Clean up pending timeout to prevent memory leak if (measurementTimeoutId.value) { @@ -170,20 +226,33 @@ onBeforeUnmount(() => {
- -
@@ -211,9 +280,9 @@ onBeforeUnmount(() => { align-items: flex-start; justify-content: flex-start; } -.floating-comment { +.comments-dialog { position: absolute; - min-width: 300px; + min-width: 290px; } .calculation-container { visibility: hidden; @@ -221,4 +290,42 @@ onBeforeUnmount(() => { left: -9999px; top: -9999px; } + +/* Page-wise comments layout */ +.page-comments-wrapper { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.page-comments-container { + position: absolute; + width: 310px; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + box-sizing: border-box; + /* Each page container is positioned to align with its document page */ + /* Comments within are positioned relative to their page container */ + /* Independent scrolling per page if content exceeds page height */ +} + +.page-comments-container::-webkit-scrollbar { + width: 6px; +} + +.page-comments-container::-webkit-scrollbar-track { + background: transparent; +} + +.page-comments-container::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +.page-comments-container::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.3); +}