From 6920d26a4d8bb2bc5cdbfdc9273bb6c912b6ccc6 Mon Sep 17 00:00:00 2001 From: Matthew Connelly Date: Tue, 26 May 2026 23:30:34 -0400 Subject: [PATCH 1/2] feat: add emitCommentEvents config to suppress sidebar bubbles Add config-level option and dynamic setters to suppress sidebar bubbles for tracked changes and comments while keeping inline marks visible. - Add `modules.trackChanges.emitCommentEvents` config option - Add `modules.comments.emitCommentEvents` config option - Add `superdoc.setTrackChangesEmitEvents()` dynamic setter - Add `superdoc.setCommentsEmitEvents()` dynamic setter - Update comments-plugin to check config for manual tracked changes - Update comments-store to check config for manual comment insertion Precedence: explicit per-call arg > config > default (true) Closes SD-3281 Co-Authored-By: Claude Opus 4.5 --- .../v1/extensions/comment/comments-plugin.js | 100 ++++++++++-------- packages/superdoc/src/SuperDoc.vue | 1 + packages/superdoc/src/core/SuperDoc.js | 44 ++++++++ packages/superdoc/src/core/types/index.js | 2 + .../superdoc/src/stores/comments-store.js | 21 ++++ 5 files changed, 124 insertions(+), 44 deletions(-) diff --git a/packages/super-editor/src/editors/v1/extensions/comment/comments-plugin.js b/packages/super-editor/src/editors/v1/extensions/comment/comments-plugin.js index cdb7b6dfdf..fe18a2edfc 100644 --- a/packages/super-editor/src/editors/v1/extensions/comment/comments-plugin.js +++ b/packages/super-editor/src/editors/v1/extensions/comment/comments-plugin.js @@ -102,27 +102,31 @@ export const CommentsPlugin = Extension.create({ if (dispatch) dispatch(tr); - // Build and emit the comment payload - const commentPayload = normalizeCommentEventPayload({ - conversation: { - commentId, - isInternal: resolvedInternal, - commentText: content, - creatorName: author ?? configUser.name, - creatorEmail: authorEmail ?? configUser.email, - creatorImage: authorImage ?? configUser.image, - createdTime: Date.now(), - }, - editorOptions: editor.options, - fallbackCommentId: commentId, - fallbackInternal: resolvedInternal, - }); + // Check if comment events should be emitted (config can suppress for external comment management) + const shouldEmitCommentEvent = editor.options.comments?.emitCommentEvents !== false; + if (shouldEmitCommentEvent) { + // Build and emit the comment payload + const commentPayload = normalizeCommentEventPayload({ + conversation: { + commentId, + isInternal: resolvedInternal, + commentText: content, + creatorName: author ?? configUser.name, + creatorEmail: authorEmail ?? configUser.email, + creatorImage: authorImage ?? configUser.image, + createdTime: Date.now(), + }, + editorOptions: editor.options, + fallbackCommentId: commentId, + fallbackInternal: resolvedInternal, + }); - editor.emit('commentsUpdate', { - type: comments_module_events.ADD, - comment: commentPayload, - activeCommentId: commentId, - }); + editor.emit('commentsUpdate', { + type: comments_module_events.ADD, + comment: commentPayload, + activeCommentId: commentId, + }); + } return true; }, @@ -156,26 +160,30 @@ export const CommentsPlugin = Extension.create({ const commentId = explicitCommentId ?? uuidv4(); const configUser = editor.options?.user || {}; - const commentPayload = normalizeCommentEventPayload({ - conversation: { - commentId, - parentCommentId: parentId, - commentText: content, - creatorName: author ?? configUser.name, - creatorEmail: authorEmail ?? configUser.email, - creatorImage: authorImage ?? configUser.image, - createdTime: Date.now(), - }, - editorOptions: editor.options, - fallbackCommentId: commentId, - fallbackInternal: false, - }); + // Check if comment events should be emitted (config can suppress for external comment management) + const shouldEmitCommentEvent = editor.options.comments?.emitCommentEvents !== false; + if (shouldEmitCommentEvent) { + const commentPayload = normalizeCommentEventPayload({ + conversation: { + commentId, + parentCommentId: parentId, + commentText: content, + creatorName: author ?? configUser.name, + creatorEmail: authorEmail ?? configUser.email, + creatorImage: authorImage ?? configUser.image, + createdTime: Date.now(), + }, + editorOptions: editor.options, + fallbackCommentId: commentId, + fallbackInternal: false, + }); - editor.emit('commentsUpdate', { - type: comments_module_events.ADD, - comment: commentPayload, - activeCommentId: commentId, - }); + editor.emit('commentsUpdate', { + type: comments_module_events.ADD, + comment: commentPayload, + activeCommentId: commentId, + }); + } return true; }, @@ -206,7 +214,9 @@ export const CommentsPlugin = Extension.create({ if (dispatch) dispatch(tr); - const shouldEmit = !skipEmit && resolvedCommentId !== 'pending'; + // Use explicit skipEmit value if provided, otherwise check config (inverted), then default to emit + const shouldSkipEmit = skipEmit ?? (this.editor.options.comments?.emitCommentEvents === false); + const shouldEmit = !shouldSkipEmit && resolvedCommentId !== 'pending'; if (shouldEmit) { const commentPayload = normalizeCommentEventPayload({ conversation, @@ -870,7 +880,9 @@ const findTrackedMark = ({ }; const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEditorState, editor) => { - const { insertedMark, deletionMark, formatMark, deletionNodes, emitCommentEvent = true } = trackedChangeMeta; + const { insertedMark, deletionMark, formatMark, deletionNodes, emitCommentEvent } = trackedChangeMeta; + // Use explicit metadata value if provided, otherwise fall back to editor config, then default to true + const shouldEmitCommentEvent = emitCommentEvent ?? editor.options.trackedChanges?.emitCommentEvents ?? true; if (!insertedMark && !deletionMark && !formatMark) { return; @@ -970,8 +982,8 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd }) : null; - if (emitCommentEvent && insertionPayload) editor.emit('commentsUpdate', insertionPayload); - if (emitCommentEvent && deletionPayload) editor.emit('commentsUpdate', deletionPayload); + if (shouldEmitCommentEvent && insertionPayload) editor.emit('commentsUpdate', insertionPayload); + if (shouldEmitCommentEvent && deletionPayload) editor.emit('commentsUpdate', deletionPayload); return newTrackedChanges; } @@ -995,7 +1007,7 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd }) : null; - if (emitParams && emitCommentEvent) editor.emit('commentsUpdate', emitParams); + if (emitParams && shouldEmitCommentEvent) editor.emit('commentsUpdate', emitParams); return newTrackedChanges; }; diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index c84844dc0b..dccea3990a 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -735,6 +735,7 @@ const editorOptions = (doc) => { comments: { highlightColors: commentsModuleConfig.value?.highlightColors, highlightOpacity: commentsModuleConfig.value?.highlightOpacity, + emitCommentEvents: commentsModuleConfig.value?.emitCommentEvents, }, trackedChanges: proxy.$superdoc.config.modules?.trackChanges, editorCtor: useLayoutEngine ? PresentationEditor : undefined, diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index f92fe56295..3fcbcd86c1 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -1321,6 +1321,50 @@ export class SuperDoc extends EventEmitter { }); } + /** + * Dynamically enable or disable comment event emission (sidebar bubbles). + * When disabled, comment marks are still added to the document but no sidebar + * entries or events are created. + * + * @param {boolean} enabled - Whether to emit comment events (default: true) + */ + setCommentsEmitEvents(enabled) { + // Update config + if (!this.config.modules) this.config.modules = {}; + if (!this.config.modules.comments) this.config.modules.comments = {}; + this.config.modules.comments.emitCommentEvents = enabled; + + // Propagate to active editors + this.superdocStore?.documents?.forEach((doc) => { + const editor = typeof doc.getEditor === 'function' ? doc.getEditor() : null; + if (editor?.options?.comments) { + editor.options.comments.emitCommentEvents = enabled; + } + }); + } + + /** + * Dynamically enable or disable tracked change comment event emission (sidebar bubbles). + * When disabled, track change marks are still applied to the document but no sidebar + * entries or events are created. + * + * @param {boolean} enabled - Whether to emit tracked change comment events (default: true) + */ + setTrackChangesEmitEvents(enabled) { + // Update config + if (!this.config.modules) this.config.modules = {}; + if (!this.config.modules.trackChanges) this.config.modules.trackChanges = {}; + this.config.modules.trackChanges.emitCommentEvents = enabled; + + // Propagate to active editors + this.superdocStore?.documents?.forEach((doc) => { + const editor = typeof doc.getEditor === 'function' ? doc.getEditor() : null; + if (editor?.options?.trackedChanges) { + editor.options.trackedChanges.emitCommentEvents = enabled; + } + }); + } + #setModeEditing() { if (this.config.role !== 'editor') return this.#setModeSuggesting(); if (this.superdocStore.documents.length > 0) { diff --git a/packages/superdoc/src/core/types/index.js b/packages/superdoc/src/core/types/index.js index 59af504f54..77dd46df7e 100644 --- a/packages/superdoc/src/core/types/index.js +++ b/packages/superdoc/src/core/types/index.js @@ -513,6 +513,7 @@ * @property {string} [comments.trackChangeActiveHighlightColors.deleteBorder] Active border color for deleted text highlight * @property {string} [comments.trackChangeActiveHighlightColors.deleteBackground] Active background color for deleted text highlight * @property {string} [comments.trackChangeActiveHighlightColors.formatBorder] Active border color for format change highlight + * @property {boolean} [comments.emitCommentEvents=true] Whether comment insertions emit comment events (creating sidebar entries). When `false`, comment marks are still applied to the document but no corresponding sidebar entries are created. This affects both programmatic and manual comment insertions. Useful when managing comments in an external system. * @property {Object} [ai] AI module configuration * @property {string} [ai.apiKey] Harbour API key for AI features * @property {string} [ai.endpoint] Custom endpoint URL for AI services @@ -547,6 +548,7 @@ * @property {'paired' | 'independent'} [replacements='paired'] How a tracked replacement (adjacent insertion + deletion created by typing over selected text) surfaces in the UI and API. * - `'paired'` (default, Google Docs model): the two halves share one id and resolve together with a single accept/reject click. * - `'independent'` (Microsoft Word / ECMA-376 §17.13.5 model): each insertion and each deletion has its own id, is addressable on its own, and resolves independently. + * @property {boolean} [emitCommentEvents=true] Whether tracked changes emit comment events (creating sidebar bubbles). When `false`, track change marks are still applied to the document but no corresponding sidebar entries are created. This affects both programmatic and manual (user typing) tracked changes. */ /** diff --git a/packages/superdoc/src/stores/comments-store.js b/packages/superdoc/src/stores/comments-store.js index bfcba4c3ca..cd51a895eb 100644 --- a/packages/superdoc/src/stores/comments-store.js +++ b/packages/superdoc/src/stores/comments-store.js @@ -720,6 +720,16 @@ export const useCommentsStore = defineStore('comments', () => { }; const showAddComment = (superdoc, targetClientY = null) => { + // When emitCommentEvents is false, add the mark directly without sidebar UI flow + const shouldEmitCommentEvents = superdoc.config.modules?.comments?.emitCommentEvents !== false; + if (!shouldEmitCommentEvents) { + // Just add the mark without the pending comment flow + if (superdoc.activeEditor?.commands) { + superdoc.activeEditor.commands.insertComment({ skipEmit: true }); + } + return; + } + const event = { type: COMMENT_EVENTS.PENDING }; superdoc.emit('comments-update', event); @@ -944,6 +954,17 @@ export const useCommentsStore = defineStore('comments', () => { * @returns {void} */ const addComment = ({ superdoc, comment, skipEditorUpdate = false, broadcastChanges = true }) => { + const shouldEmitCommentEvents = superdoc.config.modules?.comments?.emitCommentEvents !== false; + + // If emitCommentEvents is false, just add the mark without sidebar/event handling + if (!shouldEmitCommentEvents) { + if (!skipEditorUpdate && !comment.trackedChange && superdoc.activeEditor?.commands && !comment.parentCommentId) { + superdoc.activeEditor.commands.insertComment({ ...comment.getValues(), skipEmit: true }); + } + removePendingComment(superdoc); + return; + } + let parentComment = commentsList.value.find((c) => c.commentId === activeComment.value); if (!parentComment) parentComment = comment; From 2bd17694a614aef727efcfba0a07967e4048437b Mon Sep 17 00:00:00 2001 From: Matt Connerton Date: Wed, 27 May 2026 12:21:48 -0400 Subject: [PATCH 2/2] feat(dev): add testing UI for emitCommentEvents config (SD-3281) Adds controls to /dev demo for testing the bubble suppression feature: - Dropdown to toggle bubbles on/off (calls setCommentsEmitEvents and setTrackChangesEmitEvents dynamic setters) - Button to insert comment WITH bubble (programmatic, skipEmit: false) - Button to insert comment WITHOUT bubble (programmatic, skipEmit: true) Co-Authored-By: Claude Opus 4.5 --- .../src/dev/components/SuperdocDev.vue | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/superdoc/src/dev/components/SuperdocDev.vue b/packages/superdoc/src/dev/components/SuperdocDev.vue index c9f7a5e51f..cd9db73246 100644 --- a/packages/superdoc/src/dev/components/SuperdocDev.vue +++ b/packages/superdoc/src/dev/components/SuperdocDev.vue @@ -1254,6 +1254,22 @@ const closeExportMenu = () => { showExportMenu.value = false; }; +// Bubble emit controls for testing SD-3281 +const bubblesEnabled = ref(true); +const setBubblesEnabled = (enabled) => { + bubblesEnabled.value = enabled; + superdoc.value?.setCommentsEmitEvents(enabled); + superdoc.value?.setTrackChangesEmitEvents(enabled); +}; + +const insertCommentWithBubble = () => { + activeEditor.value?.commands?.insertComment({ skipEmit: false }); +}; + +const insertCommentWithoutBubble = () => { + activeEditor.value?.commands?.insertComment({ skipEmit: true }); +}; + const sidebarOptions = [ { id: 'off', @@ -1416,6 +1432,23 @@ if (scrollTestMode.value) { + + +