diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js index 1bb0d57340..8d9e1fbff6 100644 --- a/packages/super-editor/src/core/Editor.js +++ b/packages/super-editor/src/core/Editor.js @@ -363,7 +363,11 @@ export class Editor extends EventEmitter { ...options, }; - if (!this.view || !this.state || this.isDestroyed) { + if (this.options.isNewFile && this.options.isCommentsEnabled) { + this.options.shouldLoadComments = true; + } + + if (!this.view || !this.state || this.ifsDestroyed) { return; } @@ -727,6 +731,7 @@ export class Editor extends EventEmitter { #initComments(replacedFile = false) { if (!this.options.isCommentsEnabled) return; if (this.options.isHeadless) return; + if (!this.options.shouldLoadComments) return; this.emit('commentsLoaded', { editor: this, replacedFile, comments: this.converter.comments || [] }); setTimeout(() => { @@ -1077,7 +1082,8 @@ export class Editor extends EventEmitter { media, mediaFiles, fonts, - isNewFile: true + isNewFile: true, + shouldLoadComments: true, }); this.#createConverter(); diff --git a/packages/super-editor/src/extensions/comment/comments-helpers.js b/packages/super-editor/src/extensions/comment/comments-helpers.js index d5792a5d52..3b85e1f640 100644 --- a/packages/super-editor/src/extensions/comment/comments-helpers.js +++ b/packages/super-editor/src/extensions/comment/comments-helpers.js @@ -1,6 +1,6 @@ import { CommentMarkName } from './comments-constants.js'; import { COMMENTS_XML_DEFINITIONS } from '@converter/exporter-docx-defs.js'; - +import { CommentsPluginKey } from './comments-plugin.js'; /** * Remove comment by id @@ -274,3 +274,21 @@ export const translateFormatChangesToEnglish = (attrs = {}) => { return messages.length ? messages.join('. ') : 'No formatting changes.'; }; + +/** + * Get the highlight color for a comment or tracked changes node + * + * @param {Object} param0 + * @param {String} param0.activeThreadId The active comment ID + * @param {String} param0.threadId The current thread ID + * @param {Boolean} param0.isInternal Whether the comment is internal or external + * @param {EditorView} param0.editor The current editor view + * @returns {String} The color to use for the highlight + */ +export const getHighlightColor = ({ activeThreadId, threadId, isInternal, editor }) => { + if (!editor.options.isInternal && isInternal) return 'transparent'; + const pluginState = CommentsPluginKey.getState(editor.state); + const color = isInternal ? pluginState.internalColor : pluginState.externalColor; + const alpha = activeThreadId == threadId ? '44' : '22'; + return `${color}${alpha}`; +} \ No newline at end of file diff --git a/packages/super-editor/src/extensions/comment/comments-plugin.js b/packages/super-editor/src/extensions/comment/comments-plugin.js index 3fe2a72cdd..83da0e1cff 100644 --- a/packages/super-editor/src/extensions/comment/comments-plugin.js +++ b/packages/super-editor/src/extensions/comment/comments-plugin.js @@ -1,16 +1,25 @@ -import { Plugin, PluginKey } from 'prosemirror-state'; -import { Extension } from '@core/Extension.js'; -import { TrackInsertMarkName, TrackDeleteMarkName, TrackFormatMarkName } from '../track-changes/constants.js'; -import { TrackChangesBasePluginKey } from '../track-changes/plugins/index.js'; -import { Decoration, DecorationSet } from "prosemirror-view"; -import { comments_module_events } from '@harbour-enterprises/common'; -import { removeCommentsById, translateFormatChangesToEnglish } from './comments-helpers.js'; -import { CommentMarkName } from './comments-constants.js'; - -export const CommentsPluginKey = new PluginKey('comments'); +import { Plugin, PluginKey } from 'prosemirror-state' +import { Extension } from '@core/Extension.js' +import { Decoration, DecorationSet } from 'prosemirror-view' +import { removeCommentsById, getHighlightColor } from './comments-helpers.js' +import { CommentMarkName } from './comments-constants.js' +import { PaginationPluginKey } from '../pagination/pagination-helpers.js'; + + +// Example tracked-change keys, if needed +import { + TrackInsertMarkName, + TrackDeleteMarkName, + TrackFormatMarkName, +} from '../track-changes/constants.js' +import { TrackChangesBasePluginKey } from '../track-changes/plugins/index.js' +import { comments_module_events } from '@harbour-enterprises/common' +import { translateFormatChangesToEnglish } from './comments-helpers.js' const TRACK_CHANGE_MARKS = [TrackInsertMarkName, TrackDeleteMarkName, TrackFormatMarkName]; +export const CommentsPluginKey = new PluginKey('comments') + export const CommentsPlugin = Extension.create({ name: 'comments', @@ -19,184 +28,382 @@ export const CommentsPlugin = Extension.create({ insertComment: (conversation) => ({ tr, dispatch, state }) => { - const { selection } = tr; - const { $from, $to } = selection; - const { commentId, isInternal } = conversation; + const { selection } = tr + const { $from, $to } = selection + const { commentId, isInternal } = conversation - tr.setMeta(CommentsPluginKey, { event: 'add' }); + tr.setMeta(CommentsPluginKey, { event: 'add' }) tr.addMark( $from.pos, $to.pos, this.editor.schema.marks[CommentMarkName].create({ commentId, - internal: isInternal + internal: isInternal, }) - ); + ) - dispatch(tr); - return true; + dispatch(tr) + return true }, - removeComment: ({ commentId, importedId }) => ({ tr, dispatch, state }) => { - tr.setMeta(CommentsPluginKey, { event: 'deleted' }); - removeCommentsById({ commentId, importedId, state, tr, dispatch }); - }, + removeComment: + ({ commentId, importedId }) => + ({ tr, dispatch, state }) => { + tr.setMeta(CommentsPluginKey, { event: 'deleted' }) + removeCommentsById({ commentId, importedId, state, tr, dispatch }) + }, - setActiveComment: ({ commentId, importedId }) => ({ tr, dispatch, state }) => { - let activeThreadId = importedId; - if (importedId === undefined || importedId === null) activeThreadId = commentId; - tr.setMeta(CommentsPluginKey, { type: 'setActiveComment', activeThreadId }); - return true; - }, + setActiveComment: + ({ commentId, importedId }) => + ({ tr, dispatch }) => { + let activeThreadId = importedId + if (importedId === undefined || importedId === null) { + activeThreadId = commentId + } + tr.setMeta(CommentsPluginKey, { type: 'setActiveComment', activeThreadId }) + return true + }, - setCommentInternal: ({ commentId, importedId, isInternal}) => ({ tr, dispatch, state }) => { - const { doc } = state; - let foundStartNode; - let foundPos; - - // Find the commentRangeStart node that matches the comment ID - tr.setMeta(CommentsPluginKey, { event: 'update' }); - doc.descendants((node, pos) => { - if (foundStartNode) return; - - const { marks = [] } = node; - const commentMark = marks.find((mark) => mark.type.name === CommentMarkName); - - if (commentMark) { - const { attrs } = commentMark; - const wid = attrs.commentId || attrs.importedId; - if (wid == commentId || wid == importedId) { - foundStartNode = node; - foundPos = pos; + setCommentInternal: + ({ commentId, importedId, isInternal }) => + ({ tr, dispatch, state }) => { + const { doc } = state + let foundStartNode + let foundPos + + // Find the commentRangeStart node that matches the comment ID + tr.setMeta(CommentsPluginKey, { event: 'update' }) + doc.descendants((node, pos) => { + if (foundStartNode) return + + const { marks = [] } = node + const commentMark = marks.find((mark) => mark.type.name === CommentMarkName) + + if (commentMark) { + const { attrs } = commentMark + const wid = attrs.commentId || attrs.importedId + if (wid == commentId || wid == importedId) { + foundStartNode = node + foundPos = pos + } } - } - }); - - // If no matching node, return false - if (!foundStartNode) return false; - - // Update the mark itself - tr.addMark( - foundPos, - foundPos + foundStartNode.nodeSize, - this.editor.schema.marks[CommentMarkName].create({ - commentId, - internal: isInternal, }) - ); - - // Let comments plugin know we need to update - tr.setMeta(CommentsPluginKey, { type: 'setCommentInternal' }); - dispatch(tr); - return true; - }, - resolveComment: ({ commentId, importedId }) => ({ tr, dispatch, state }) => { - tr.setMeta(CommentsPluginKey, { event: 'update' }); - removeCommentsById({ commentId, importedId, state, tr, dispatch }); - }, + // If no matching node, return false + if (!foundStartNode) return false + + // Update the mark itself + tr.addMark( + foundPos, + foundPos + foundStartNode.nodeSize, + this.editor.schema.marks[CommentMarkName].create({ + commentId, + internal: isInternal, + }) + ) + + tr.setMeta(CommentsPluginKey, { type: 'setCommentInternal' }) + dispatch(tr) + return true + }, - }; + resolveComment: + ({ commentId, importedId }) => + ({ tr, dispatch, state }) => { + tr.setMeta(CommentsPluginKey, { event: 'update' }) + removeCommentsById({ commentId, importedId, state, tr, dispatch }) + }, + } }, addPmPlugins() { - const editor = this.editor; - let hasInitialized = false; + const editor = this.editor + let shouldUpdate; + const commentsPlugin = new Plugin({ key: CommentsPluginKey, + state: { - init(_, { doc, selection }) { + init() { return { - decorations: DecorationSet.empty, activeThreadId: null, - allCommentIds: [], externalColor: '#B1124B', internalColor: '#078383', + decorations: DecorationSet.empty, + allCommentPositions: {}, + allCommentIds: [], + changedActiveThread: false, trackedChanges: {}, - }; + } }, - apply(tr, oldState, _, newEditorState) { - if (!editor.options.isCommentsEnabled) return { ...oldState }; - let activeThreadId; - let isForcingUpdate = false; - const { selection } = tr; - const doc = newEditorState.doc; - const meta = tr.getMeta(CommentsPluginKey); + apply(tr, pluginState, _, newEditorState) { - // If we have plugin meta, we will force update - if (meta?.type === 'force') isForcingUpdate = true; + const paginationMeta = tr.getMeta(PaginationPluginKey); + const isPaginationInit = paginationMeta?.isReadyToInit; + if (isPaginationInit) shouldUpdate = true; + + const meta = tr.getMeta(CommentsPluginKey); + if (!isPaginationInit && meta && meta.decorations) { + return { + ...pluginState, + decorations: meta.decorations, + allCommentPositions: meta.allCommentPositions, + } + }; // If this is a tracked change transaction, handle separately const trackedChangeMeta = tr.getMeta(TrackChangesBasePluginKey); - let currentTrackedChanges = oldState.trackedChanges; + const currentTrackedChanges = pluginState.trackedChanges; if (trackedChangeMeta) { - currentTrackedChanges = handleTrackedChangeTransaction( + pluginState.trackedChanges = handleTrackedChangeTransaction( trackedChangeMeta, currentTrackedChanges, newEditorState, editor, ); + }; + + const trChangedActiveComment = meta?.type === 'setActiveComment'; + if ((!tr.docChanged && tr.selectionSet) || trChangedActiveComment) { + + const { selection } = tr; + let activeThreadId = getActiveCommentId(newEditorState.doc, selection); + if (trChangedActiveComment) activeThreadId = meta.activeThreadId; + + const previousSelectionId = pluginState.activeThreadId; + if (previousSelectionId !== activeThreadId || trChangedActiveComment) { + pluginState.activeThreadId = activeThreadId; + const update = { + type: comments_module_events.SELECTED, + activeCommentId: activeThreadId ? activeThreadId : null + }; + + editor.emit('commentsUpdate', update); + pluginState.changedActiveThread = true; + } else { + pluginState.changedActiveThread = false; + } }; - // If we have a new active comment ID, we will update it - if (meta?.type === 'setActiveComment') { - isForcingUpdate = true; - activeThreadId = meta.activeThreadId; - } - - // If the document hasn't changed, return the old state - if (!isForcingUpdate && hasInitialized && !tr.docChanged && !tr.selectionSet) return { ...oldState } - - // If the selection changes, check if we're inside a comment - if (!isForcingUpdate && hasInitialized && !tr.docChanged && tr.selectionSet) { - const previousSelectionId = oldState.activeThreadId; - activeThreadId = getActiveCommentId(doc, selection); - - // If the comment selection didn't change, return the old state - if (previousSelectionId === activeThreadId) return { ...oldState }; - - // Otherwise, we update the active comment ID - const update = { - type: comments_module_events.SELECTED, - activeCommentId: activeThreadId ? activeThreadId : null - }; - - editor.emit('commentsUpdate', update); - }; - - // Generate decorations for comment highlights - const { - decorations, - allCommentIds, - allCommentPositions - } = processDocumentComments(editor, doc, activeThreadId, oldState) || {}; - const decorationSet = DecorationSet.create(doc, decorations); - - // Emit the comment-positions event which signals that comments might have changed - // SuperDoc will use this to update floating comments as necessary + const { allCommentIds, allCommentPositions } = pluginState; editor.emit('comment-positions', { allCommentIds, allCommentPositions }); - hasInitialized = true; - return { - ...oldState, - activeThreadId, - allCommentIds, - decorations: decorationSet, - trackedChanges: currentTrackedChanges, - }; + return pluginState }, }, + props: { decorations(state) { - return this.getState(state).decorations; + return this.getState(state).decorations + }, + }, + + view(editorView) { + let prevDoc; + let prevDecorations; + + return { + update(view, prevState) { + const { state } = view + const { doc, tr } = state + + const pluginState = CommentsPluginKey.getState(state) + const { activeThreadId} = pluginState; + + if (prevDoc && prevDoc.eq(doc) || !shouldUpdate) return; + prevDoc = doc; + + const decorations = [] + const allCommentPositions = {} + doc.descendants((node, pos) => { + const { marks = [] } = node + const commentMarks = marks.filter( + (mark) => mark.type.name === CommentMarkName + ) + + commentMarks.forEach((commentMark) => { + const { attrs } = commentMark + const threadId = attrs.commentId || attrs.importedId + + const currentBounds = view.coordsAtPos(pos) + + updatePosition({ + allCommentPositions, + threadId, + pos, + currentBounds, + node, + }); + + const isInternal = attrs.internal; + const color = getHighlightColor({ activeThreadId, threadId, isInternal, editor }); + const deco = Decoration.inline(pos, pos + node.nodeSize, { + style: `background-color: ${color}`, + class: 'comment-highlight', + 'data-thread-id': threadId, + }) + decorations.push(deco) + }); + + const trackedChangeNode = getTrackedChangeNode(node); + if (trackedChangeNode) { + const currentBounds = view.coordsAtPos(pos); + const { id } = trackedChangeNode.attrs; + updatePosition({ + allCommentPositions, + threadId: id, + pos, + currentBounds, + node, + }); + }; + }); + + const decorationSet = DecorationSet.create(doc, decorations) + + // Compare new decorations with the old state to avoid infinite loop + const oldDecorations = pluginState.decorations + + // We only dispatch if something actually changed + const same = oldDecorations.eq(decorationSet) + if (!same) { + const tr = state.tr.setMeta(CommentsPluginKey, { + decorations: decorationSet, + allCommentPositions, + }) + // Dispatch the transaction to update pluginState + view.dispatch(tr) + } + + editor.emit('comment-positions', { allCommentPositions }); + + // Remember the new decorations for next time + prevDecorations = decorationSet + }, } - } - }); - return [commentsPlugin]; + }, + }) + + return [commentsPlugin] }, }); +const updatePosition = ({ + allCommentPositions, + threadId, + pos, + currentBounds, + node, +}) => { + if (!allCommentPositions[threadId]) { + allCommentPositions[threadId] = { + threadId, + start: pos, + end: pos + node.nodeSize, + bounds: { ...currentBounds }, + } + } else { + // Adjust the positional indices + const existing = allCommentPositions[threadId] + existing.start = Math.min(existing.start, pos) + existing.end = Math.max(existing.end, pos + node.nodeSize) + existing.bounds.top = Math.min(existing.bounds.top, currentBounds.top) + existing.bounds.bottom = Math.max(existing.bounds.bottom, currentBounds.bottom) + } +}; + +/** + * This is run when a new selection is set (tr.selectionSet) to return the active comment ID, if any + * If there are multiple, only return the first one + * + * @param {Object} doc The current document + * @param {Selection} selection The current selection + * @returns {String | null} The active comment ID, if any + */ +const getActiveCommentId = (doc, selection) => { + if (!selection) return; + const { $from, $to } = selection; + + // We only need to check for active comment ID if the selection is empty + if ($from.pos !== $to.pos) return; + + const nodeAtPos = doc.nodeAt($from.pos); + if (!nodeAtPos) return; + + // If we have a tracked change, we can return it right away + const trackedChangeNode = getTrackedChangeNode(nodeAtPos); + if (trackedChangeNode) { + return trackedChangeNode.attrs.wid; + } + + // Otherwise, we need to check for comment nodes + const overlaps = []; + let found = false; + + // Look for commentRangeStart nodes before the current position + // There could be overlapping comments so we need to track all of them + doc.descendants((node, pos) => { + if (found) return; + + // node goes from `pos` to `end = pos + node.nodeSize` + const end = pos + node.nodeSize; + + // If $from.pos is outside this node’s range, skip it + if ($from.pos < pos || $from.pos >= end) { + return; + } + + // Now we know $from.pos is within this node’s start/end + const { marks = [] } = node; + const commentMark = marks.find((mark) => mark.type.name === CommentMarkName); + if (commentMark) { + overlaps.push({ + node, + pos, + size: node.nodeSize, + }); + } + + // If we've passed the position, we can stop + if (pos > $from.pos) { + found = true; + } + }); + + // Get the closest commentRangeStart node to the current position + let closest = null; + let closestCommentRangeStart = null; + overlaps.forEach(({ pos, node }) => { + if (!closest) closest = $from.pos - pos; + + const diff = $from.pos - pos; + if (diff >= 0 && diff <= closest) { + closestCommentRangeStart = node; + closest = diff; + } + }); + + const { marks: closestMarks = [] } = closestCommentRangeStart || {}; + const closestCommentMark = closestMarks.find((mark) => mark.type.name === CommentMarkName); + return closestCommentMark?.attrs?.commentId || closestCommentMark?.attrs?.importedId; +}; + + +/** + * Check if this node is a tracked changes node + * @param {Node} node The node to check + * @returns {Node | null} Either a tracked change node (insert, delete) or null + */ +const getTrackedChangeNode = (node) => { + if (!node) return; + const nodeMarks = node.marks; + const trackedChangeMark = nodeMarks?.find((mark) => mark.type.name === TrackInsertMarkName); + const trackedDeleteMark = nodeMarks?.find((mark) => mark.type.name === TrackDeleteMarkName); + const trackedFormatMark = nodeMarks?.find((mark) => mark.type.name === TrackFormatMarkName); + return trackedChangeMark || trackedDeleteMark || trackedFormatMark; +}; + const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEditorState, editor) => { const { deletionMark, insertedMark, formatMark, deletionNodes } = trackedChangeMeta; if (!deletionMark && !insertedMark && !formatMark) return; @@ -229,8 +436,6 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd }; }); } - - const coords = editor.view.coordsAtPos(step.to); const emitParams = createOrUpdateTrackedChangeComment({ documentId: editor.options.documentId, event: isNewChange ? 'add' : 'update', @@ -242,7 +447,6 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd deletionNodes, nodes: nodes, newEditorState, - coords, }); if (emitParams) editor.emit('commentsUpdate', emitParams); @@ -265,15 +469,7 @@ const getTrackedChangeText = ({ node, mark, trackedChangeType, isDeletionInserti } }; -const createOrUpdateTrackedChangeComment = ({ - event, - marks, - deletionNodes, - nodes, - newEditorState, - documentId, - coords, -}) => { +const createOrUpdateTrackedChangeComment = ({ event, marks, deletionNodes, nodes, newEditorState, documentId }) => { const trackedMark = marks.insertedMark || marks.deletionMark || marks.formatMark; const { type, attrs } = trackedMark; @@ -321,217 +517,9 @@ const createOrUpdateTrackedChangeComment = ({ author, authorEmail, date, - coords, }; if (event === 'add') params.event = comments_module_events.ADD; else if (event === 'update') params.event = comments_module_events.UPDATE; return params; -}; - -/** - * Check if this node is a tracked changes node - * @param {Node} node The node to check - * @returns {Node | null} Either a tracked change node (insert, delete) or null - */ -const getTrackedChangeNode = (node) => { - if (!node) return; - const nodeMarks = node.marks; - const trackedChangeMark = nodeMarks?.find((mark) => mark.type.name === TrackInsertMarkName); - const trackedDeleteMark = nodeMarks?.find((mark) => mark.type.name === TrackDeleteMarkName); - const trackedFormatMark = nodeMarks?.find((mark) => mark.type.name === TrackFormatMarkName); - return trackedChangeMark || trackedDeleteMark || trackedFormatMark; -}; - - - - -/** - * Main function to track comment and tracked change nodes - * - * @param {EditorView} view The current editor view - * @param {Object} allCommentPositions The current positions of nodes being tracked - * @param {Node} node The current node to consider - * @param {Number} pos The position of the node - * @returns {void} allCommentPositions is modified in place - */ -const trackCommentNodes = ({ - allCommentPositions, - decorations, - node, - pos, - editor, - activeThreadId, -}) => { - // Check if it contains the commentMarkName - const { marks = [] } = node; - - // Check if this is a comment node (ie: has commentMark) - const commentMarks = marks.filter((mark) => mark.type.name === CommentMarkName); - commentMarks.forEach((commentMark) => { - const { attrs } = commentMark; - const threadId = attrs.commentId || attrs.importedId; - const isInternal = attrs.internal; - const color = getHighlightColor({ activeThreadId, threadId, isInternal, editor }); - - const bounds = editor.view.coordsAtPos(pos); - const deco = Decoration.inline( - pos, - pos + node.nodeSize, - { - style: `background-color: ${color};`, - class: 'comment-highlight', - 'data-thread-id': threadId, - } - ); - decorations.push(deco); - - if (threadId === 'pending' || allCommentPositions[threadId]) return; - - allCommentPositions[threadId] = { - threadId, - start: pos, - end: pos + node.nodeSize, - internal: isInternal, - bounds, - }; - }); - - const trackedChangeNode = getTrackedChangeNode(node); - if (trackedChangeNode) { - const threadId = trackedChangeNode.attrs.id; - const bounds = editor.view.coordsAtPos(pos); - - allCommentPositions[threadId] = { - threadId, - start: pos, - end: pos + node.nodeSize, - internal: false, - bounds, - }; - } -}; - -/** - * Get the highlight color for a comment or tracked changes node - * - * @param {Object} param0 - * @param {String} param0.activeThreadId The active comment ID - * @param {String} param0.threadId The current thread ID - * @param {Boolean} param0.isInternal Whether the comment is internal or external - * @param {EditorView} param0.editor The current editor view - * @returns {String} The color to use for the highlight - */ -const getHighlightColor = ({ activeThreadId, threadId, isInternal, editor }) => { - if (!editor.options.isInternal && isInternal) return 'transparent'; - const pluginState = CommentsPluginKey.getState(editor.state); - const color = isInternal ? pluginState.internalColor : pluginState.externalColor; - const alpha = activeThreadId == threadId ? '44' : '22'; - return `${color}${alpha}`; -} - -/** - * Iterate through the document to track comment and tracked changes nodes - * @param {*} editor The current editor instance - * @param {*} doc The current document - * @returns {Object} The positions of all tracked nodes where keys are the thread IDs - */ -const processDocumentComments = (editor, doc, activeThreadId, pluginState) => { - const allCommentPositions = {}; - const decorations = []; - - doc.descendants((node, pos) => { - trackCommentNodes({ - allCommentPositions, decorations, node, pos, editor, doc, activeThreadId, pluginState, - }); - }); - - // Get all current thread IDs in the document - const allCommentIds = Object.keys(allCommentPositions) - .map((threadId) => threadId) - .filter((threadId) => threadId !== 'pending'); - - return { - decorations, - allCommentIds, - allCommentPositions, - }; -}; - -/** - * This is run when a new selection is set (tr.selectionSet) to return the active comment ID, if any - * If there are multiple, only return the first one - * - * @param {Object} doc The current document - * @param {Selection} selection The current selection - * @returns {String | null} The active comment ID, if any - */ -const getActiveCommentId = (doc, selection) => { - if (!selection) return; - const { $from, $to } = selection; - - // We only need to check for active comment ID if the selection is empty - if ($from.pos !== $to.pos) return; - - const nodeAtPos = doc.nodeAt($from.pos); - if (!nodeAtPos) return; - - // If we have a tracked change, we can return it right away - const trackedChangeNode = getTrackedChangeNode(nodeAtPos); - if (trackedChangeNode) { - return trackedChangeNode.attrs.wid; - } - - // Otherwise, we need to check for comment nodes - const overlaps = []; - let found = false; - - // Look for commentRangeStart nodes before the current position - // There could be overlapping comments so we need to track all of them - doc.descendants((node, pos) => { - if (found) return; - - // node goes from `pos` to `end = pos + node.nodeSize` - const end = pos + node.nodeSize; - - // If $from.pos is outside this node’s range, skip it - if ($from.pos < pos || $from.pos >= end) { - return; - } - - // Now we know $from.pos is within this node’s start/end - const { marks = [] } = node; - const commentMark = marks.find((mark) => mark.type.name === CommentMarkName); - if (commentMark) { - overlaps.push({ - node, - pos, - size: node.nodeSize, - }); - } - - // If we've passed the position, we can stop - if (pos > $from.pos) { - found = true; - } - }); - - // Get the closest commentRangeStart node to the current position - let closest = null; - let closestCommentRangeStart = null; - overlaps.forEach(({ pos, node }) => { - if (!closest) closest = $from.pos - pos; - - const diff = $from.pos - pos; - if (diff >= 0 && diff <= closest) { - closestCommentRangeStart = node; - closest = diff; - } - }); - - const { marks: closestMarks = [] } = closestCommentRangeStart || {}; - const closestCommentMark = closestMarks.find((mark) => mark.type.name === CommentMarkName); - return closestCommentMark?.attrs?.commentId || closestCommentMark?.attrs?.importedId; -}; - - +}; \ No newline at end of file diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index c46caccb82..d7822826fc 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -59,6 +59,7 @@ const { generalCommentIds, getFloatingComments, hasSyncedCollaborationComments, + hasInitializedLocations, } = storeToRefs(commentsStore); const { initialCheck, showAddComment, handleEditorLocationsUpdate, handleTrackedChangeUpdate } = commentsStore; const { proxy } = getCurrentInstance(); @@ -111,11 +112,13 @@ const cancelPendingComment = (e) => { }; const onCommentsLoaded = ({ editor, comments, replacedFile }) => { - if (editor.options.isNewFile || replacedFile) { - commentsStore.processLoadedDocxComments({ - superdoc: proxy.$superdoc, - comments, - documentId: editor.options.documentId + if (editor.options.shouldLoadComments || replacedFile) { + nextTick(() => { + commentsStore.processLoadedDocxComments({ + superdoc: proxy.$superdoc, + comments, + documentId: editor.options.documentId + }); }); } }; @@ -569,7 +572,7 @@ const handlePdfClick = (e) => { -