Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
};
Expand Down
1 change: 1 addition & 0 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions packages/superdoc/src/core/SuperDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/superdoc/src/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/

/**
Expand Down
33 changes: 33 additions & 0 deletions packages/superdoc/src/dev/components/SuperdocDev.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -1416,6 +1432,23 @@ if (scrollTestMode.value) {
<option value="independent">Independent (Word)</option>
</select>
</label>
<label class="dev-app__theme-control" title="Toggle comment/track-change sidebar bubbles (SD-3281)">
<span>Bubbles</span>
<select
:value="bubblesEnabled ? 'on' : 'off'"
class="dev-app__theme-select"
@change="setBubblesEnabled($event.target.value === 'on')"
>
<option value="on">On</option>
<option value="off">Off</option>
</select>
</label>
<button class="dev-app__header-export-btn" title="Insert comment with bubble (SD-3281)" @click="insertCommentWithBubble">
Comment +bubble
</button>
<button class="dev-app__header-export-btn" title="Insert comment without bubble (SD-3281)" @click="insertCommentWithoutBubble">
Comment −bubble
</button>
<div class="dev-app__dropdown" @mouseleave="closeSidebarMenu">
<button
class="dev-app__header-export-btn dev-app__dropdown-trigger"
Expand Down
21 changes: 21 additions & 0 deletions packages/superdoc/src/stores/comments-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;

Expand Down
Loading