From 0c8b2427e497fd3c050022a68c2a4a7191ba8bfb Mon Sep 17 00:00:00 2001 From: cam Date: Sun, 18 Jan 2026 22:48:47 -0800 Subject: [PATCH] feat(comments): add anchor helpers --- .../superdoc/src/stores/comments-store.js | 52 +++++++++++++++++++ .../src/stores/comments-store.test.js | 35 +++++++++++++ 2 files changed, 87 insertions(+) diff --git a/packages/superdoc/src/stores/comments-store.js b/packages/superdoc/src/stores/comments-store.js index 7d0644390..fadc3ddd9 100644 --- a/packages/superdoc/src/stores/comments-store.js +++ b/packages/superdoc/src/stores/comments-store.js @@ -93,6 +93,55 @@ export const useCommentsStore = defineStore('comments', () => { return getComment(comment.parentCommentId); }; + const getCommentPositionKey = (commentOrId) => { + if (!commentOrId) return null; + if (typeof commentOrId === 'object') { + return commentOrId.commentId ?? commentOrId.importedId ?? null; + } + return commentOrId; + }; + + const getCommentPositionRange = (position) => { + if (!position) return null; + const start = position.start ?? position.pos ?? position.from; + const end = position.end ?? position.to ?? start; + if (!Number.isFinite(start) || !Number.isFinite(end)) return null; + return { start, end }; + }; + + const getCommentPosition = (commentOrId) => { + const key = getCommentPositionKey(commentOrId); + if (!key) return null; + return editorCommentPositions.value?.[key] ?? null; + }; + + const getCommentAnchoredText = (commentOrId, options = {}) => { + const comment = typeof commentOrId === 'object' ? commentOrId : getComment(commentOrId); + if (!comment) return null; + + const position = getCommentPosition(comment); + const range = getCommentPositionRange(position); + if (!range) return null; + + const doc = superdocStore.getDocument(comment.fileId); + const editor = doc?.getEditor?.(); + const docNode = editor?.state?.doc; + if (!docNode?.textBetween) return null; + + const separator = options.separator ?? ' '; + const text = docNode.textBetween(range.start, range.end, separator, separator); + return options.trim === false ? text : text?.trim(); + }; + + const getCommentAnchorData = (commentOrId, options = {}) => { + const position = getCommentPosition(commentOrId); + if (!position) return null; + return { + position, + anchoredText: getCommentAnchoredText(commentOrId, options), + }; + }; + const isThreadVisible = (comment) => { if (!isViewingMode.value) return true; const parent = getThreadParent(comment); @@ -711,6 +760,9 @@ export const useCommentsStore = defineStore('comments', () => { documentsWithConverations, getGroupedComments, getFloatingComments, + getCommentPosition, + getCommentAnchoredText, + getCommentAnchorData, // Actions init, diff --git a/packages/superdoc/src/stores/comments-store.test.js b/packages/superdoc/src/stores/comments-store.test.js index 65dd1272c..c954b2350 100644 --- a/packages/superdoc/src/stores/comments-store.test.js +++ b/packages/superdoc/src/stores/comments-store.test.js @@ -394,4 +394,39 @@ describe('comments-store', () => { expect(store.getGroupedComments.parentComments).toEqual([]); }); }); + + describe('comment anchor helpers', () => { + it('returns comment position by id or comment object', () => { + const comment = { commentId: 'c-1', fileId: 'doc-1' }; + store.commentsList = [comment]; + store.editorCommentPositions = { + 'c-1': { start: 12, end: 18 }, + }; + + expect(store.getCommentPosition('c-1')).toEqual({ start: 12, end: 18 }); + expect(store.getCommentPosition(comment)).toEqual({ start: 12, end: 18 }); + }); + + it('returns anchored text when editor and positions are available', () => { + const textBetween = vi.fn(() => 'Anchored text'); + const editorStub = { state: { doc: { textBetween } } }; + __mockSuperdoc.documents.value = [{ id: 'doc-1', type: 'docx', getEditor: () => editorStub }]; + + store.commentsList = [{ commentId: 'c-1', fileId: 'doc-1' }]; + store.editorCommentPositions = { + 'c-1': { start: 5, end: 12 }, + }; + + expect(store.getCommentAnchoredText('c-1')).toBe('Anchored text'); + expect(textBetween).toHaveBeenCalledWith(5, 12, ' ', ' '); + }); + + it('returns null when position or editor is missing', () => { + store.commentsList = [{ commentId: 'c-1', fileId: 'doc-1' }]; + store.editorCommentPositions = {}; + + expect(store.getCommentAnchoredText('c-1')).toBeNull(); + expect(store.getCommentAnchorData('c-1')).toBeNull(); + }); + }); });