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/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) { + + +