Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ watch(getFloatingComments, () => {
.floating-comments {
min-width: 300px;
width: 300px;
height: 100%;
}

.superdoc__layers {
Expand Down
13 changes: 11 additions & 2 deletions packages/superdoc/src/components/CommentsLayer/CommentDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ onMounted(() => {
@overflow-select="handleOverflowSelect($event, comment)"
/>

<div class="card-section comment-body" v-if="comment.trackedChange">
<div class="card-section comment-body" :class="{ 'is-active': isActiveComment }" v-if="comment.trackedChange">
<div class="tracked-change">
<div class="tracked-change">
<div v-if="comment.trackedChangeType === 'trackFormat'">
Expand All @@ -400,7 +400,7 @@ onMounted(() => {
</div>

<!-- Show the comment text, unless we enter edit mode, then show an input and update buttons -->
<div class="card-section comment-body" v-if="!comment.trackedChange">
<div class="card-section comment-body" :class="{ 'is-active': isActiveComment }" v-if="!comment.trackedChange">
<div v-if="!isDebugging && !isEditingThisComment(comment)" class="comment" v-html="comment.commentText"></div>
<div v-else-if="isDebugging && !isEditingThisComment(comment)" class="comment">
{{
Expand Down Expand Up @@ -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;
Expand Down
195 changes: 151 additions & 44 deletions packages/superdoc/src/components/CommentsLayer/FloatingComments.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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' };
Expand All @@ -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;
Expand Down Expand Up @@ -76,24 +137,44 @@ 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] = [];
acc[key].push(comment);
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();
Expand All @@ -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) {
Expand All @@ -170,20 +226,33 @@ onBeforeUnmount(() => {
</div>
</div>

<!-- Second group: Render only after first group is processed -->
<div v-if="firstGroupRendered" class="sidebar-container" :style="{ top: verticalOffset + 'px' }">
<!-- Second group: Render by page after first group is processed -->
<div v-if="firstGroupRendered" class="page-comments-wrapper">
<div
v-for="comment in renderedSizes"
:key="comment.id"
:style="getCommentPosition(comment)"
class="floating-comment"
v-for="(comments, pageIndex) in commentsByPage"
:key="`page-${pageIndex}`"
class="page-comments-container"
:data-page-index="pageIndex"
:style="getPageContainerStyle(pageIndex)"
:ref="
(el) => {
if (el) pageContainerRefs[pageIndex] = el;
}
"
>
<CommentDialog
:key="comment.id + commentsRenderKey"
<div
v-for="comment in comments"
:key="comment.id"
:style="getCommentPosition(comment)"
class="floating-comment"
:parent="parent"
:comment="comment.commentRef"
/>
>
<CommentDialog
:key="comment.id + commentsRenderKey"
class="floating-comment"
:parent="parent"
:comment="comment.commentRef"
/>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -211,14 +280,52 @@ 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;
position: fixed;
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);
}
</style>
Loading