diff --git a/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js b/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js index 8425cf9924..85f8d8834d 100644 --- a/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js +++ b/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js @@ -100,8 +100,9 @@ export const getCommentDefinition = (comment, commentId, allComments) => { 'w15:paraId': comment.commentParaId, 'custom:internalId': comment.commentId || comment.internalId, 'custom:trackedChange': comment.trackedChange, - 'custom:trackedChangeText': comment.trackedChangeText, + 'custom:trackedChangeText': comment.trackedChangeText || null, 'custom:trackedChangeType': comment.trackedChangeType, + 'custom:trackedDeletedText': comment.deletedText || null, }; // Add the w15:paraIdParent attribute if the comment has a parent @@ -173,6 +174,7 @@ export const updateCommentsXml = (commentDefs = [], commentsXml) => { 'custom:trackedChange': commentDef.attributes['custom:trackedChange'], 'custom:trackedChangeText': commentDef.attributes['custom:trackedChangeText'], 'custom:trackedChangeType': commentDef.attributes['custom:trackedChangeType'], + 'custom:trackedDeletedText': commentDef.attributes['custom:trackedDeletedText'], 'xmlns:custom': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', }; }); diff --git a/packages/super-editor/src/core/super-converter/v2/importer/documentCommentsImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/documentCommentsImporter.js index 89fe73d16b..13baa54084 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/documentCommentsImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/documentCommentsImporter.js @@ -30,8 +30,13 @@ export function importCommentData({ docx }) { const createdDate = attributes['w:date']; const internalId = attributes['custom:internalId']; const trackedChange = attributes['custom:trackedChange'] === 'true'; - const trackedChangeText = attributes['custom:trackedChangeText']; const trackedChangeType = attributes['custom:trackedChangeType']; + const trackedChangeText = attributes['custom:trackedChangeText'] !== 'null' + ? attributes['custom:trackedChangeText'] + : null; + const trackedDeletedText = attributes['custom:trackedDeletedText'] !== 'null' + ? attributes['custom:trackedDeletedText'] + : null; const date = new Date(createdDate); const unixTimestampMs = date.getTime(); @@ -59,6 +64,7 @@ export function importCommentData({ docx }) { trackedChange, trackedChangeText, trackedChangeType, + trackedDeletedText, }; }); diff --git a/packages/super-editor/src/extensions/comment/comments-plugin.js b/packages/super-editor/src/extensions/comment/comments-plugin.js index ae77ca74cd..a1fb9ec76a 100644 --- a/packages/super-editor/src/extensions/comment/comments-plugin.js +++ b/packages/super-editor/src/extensions/comment/comments-plugin.js @@ -344,7 +344,7 @@ const getActiveCommentId = (doc, selection) => { // If we have a tracked change, we can return it right away const trackedChangeNode = getTrackedChangeNode(nodeAtPos); if (trackedChangeNode) { - return trackedChangeNode.attrs.wid; + return trackedChangeNode.attrs.id; } // Otherwise, we need to check for comment nodes @@ -408,19 +408,30 @@ const getActiveCommentId = (doc, selection) => { const getTrackedChangeNode = (node) => { if (!node) return; const nodeMarks = node.marks; - const trackedChangeMark = nodeMarks?.find((mark) => mark.type.name === TrackInsertMarkName); + const trackedInsertMark = 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; + return trackedInsertMark || trackedDeleteMark || trackedFormatMark; }; const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEditorState, editor) => { - const { deletionMark, insertedMark, formatMark, deletionNodes } = trackedChangeMeta; - if (!deletionMark && !insertedMark && !formatMark) return; + const { + insertedMark, + deletionMark, + formatMark, + deletionNodes, + } = trackedChangeMeta; + + if (!insertedMark && !deletionMark && !formatMark) { + return; + } const newTrackedChanges = { ...trackedChanges }; let id = insertedMark?.attrs?.id || deletionMark?.attrs?.id || formatMark?.attrs?.id; - if (!id) return trackedChanges; + + if (!id) { + return trackedChanges; + } // Maintain a map of tracked changes with their inserted/deleted ids let isNewChange = false; @@ -446,6 +457,7 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd }; }); } + const emitParams = createOrUpdateTrackedChangeComment({ documentId: editor.options.documentId, event: isNewChange ? 'add' : 'update', @@ -455,7 +467,7 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd formatMark, }, deletionNodes, - nodes: nodes, + nodes, newEditorState, }); @@ -464,21 +476,47 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd return newTrackedChanges; }; -const getTrackedChangeText = ({ node, mark, trackedChangeType, isDeletionInsertion, deletionNodes = [] }) => { - const deletionText = deletionNodes.length ? deletionNodes[0].text : null; +const getTrackedChangeText = ({ + state, + node, + mark, + marks, + trackedChangeType, + isDeletionInsertion, + deletionNodes = [], +}) => { + let trackedChangeText = ''; + let deletionText = ''; - let nextNode = null; // - let trackedChangeText = isDeletionInsertion ? nextNode?.text : node?.text; - if (!trackedChangeText) trackedChangeText = ''; + if (trackedChangeType === TrackInsertMarkName) { + trackedChangeText = node?.text ?? ''; + } // If this is a format change, let's get the string of what changes were made - const isFormatChange = trackedChangeType === TrackFormatMarkName; - if (isFormatChange) trackedChangeText = translateFormatChangesToEnglish(mark.attrs) + if (trackedChangeType === TrackFormatMarkName) { + trackedChangeText = translateFormatChangesToEnglish(mark.attrs); + } + + if (trackedChangeType === TrackDeleteMarkName || isDeletionInsertion) { + deletionText = node?.text ?? ''; + + if (isDeletionInsertion) { + let { id } = marks.deletionMark.attrs; + let deletionNode = findNode(state.doc, (node) => { + const { marks = [] } = node; + const changeMarks = marks.filter((mark) => TRACK_CHANGE_MARKS.includes(mark.type.name)); + if (!changeMarks.length) return false; + const hasMatchingId = changeMarks.find((mark) => mark.attrs.id === id); + if (hasMatchingId) return true; + }); + deletionText = deletionNode?.node.text ?? ''; + } + } return { deletionText, trackedChangeText, - } + }; }; const createOrUpdateTrackedChangeComment = ({ event, marks, deletionNodes, nodes, newEditorState, documentId }) => { @@ -487,39 +525,34 @@ const createOrUpdateTrackedChangeComment = ({ event, marks, deletionNodes, nodes const { name: trackedChangeType } = type; const { author, authorEmail, date } = attrs; - - let id = attrs.id; - - // if (!nodes.length) { - // return; - // } + const id = attrs.id; const node = nodes[0]; - const nextTrackedNode = null; // - const isDeletionInsertion = ( - trackedChangeType === TrackDeleteMarkName && nextTrackedNode?.type?.name === TrackInsertMarkName - ); + const isDeletionInsertion = !!(marks.insertedMark && marks.deletionMark); let existingNode; newEditorState.doc.descendants((node, pos) => { const { marks = [] } = node; const changeMarks = marks.filter((mark) => TRACK_CHANGE_MARKS.includes(mark.type.name)); if (!changeMarks.length) return; - const hasMatchingId = changeMarks.find((mark) => mark.attrs.id === id); if (hasMatchingId) existingNode = node; - if (!existingNode) return false; + if (existingNode) return false; }); const { deletionText, trackedChangeText } = getTrackedChangeText({ + state: newEditorState, node: existingNode || node, mark: trackedMark, + marks, trackedChangeType, isDeletionInsertion, - deletionNodes + deletionNodes, }); - if (!deletionText && !trackedChangeText) return; + if (!deletionText && !trackedChangeText) { + return; + } const params = { event: comments_module_events.ADD, @@ -536,5 +569,15 @@ const createOrUpdateTrackedChangeComment = ({ event, marks, deletionNodes, nodes if (event === 'add') params.event = comments_module_events.ADD; else if (event === 'update') params.event = comments_module_events.UPDATE; + return params; -}; \ No newline at end of file +}; + +function findNode(node, predicate) { + let found = null; + node.descendants((node, pos) => { + if (predicate(node)) found = { node, pos }; + if (found) return false; + }); + return found; +} diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js index 1add579e84..605ead47b9 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js @@ -4,7 +4,7 @@ import { Node } from 'prosemirror-model'; import { TrackDeleteMarkName, TrackFormatMarkName } from '../constants.js'; import { v4 as uuidv4 } from 'uuid'; import { objectIncludes } from '@core/utilities/objectIncludes.js'; -import { TrackChangesBasePluginKey } from '../plugins/index.js'; +import { TrackChangesBasePluginKey } from '../plugins/trackChangesBasePlugin.js'; /** * Add mark step. @@ -19,6 +19,7 @@ import { TrackChangesBasePluginKey } from '../plugins/index.js'; */ export const addMarkStep = ({ state, tr, step, newTr, map, doc, user, date }) => { const meta = {}; + doc.nodesBetween(step.from, step.to, (node, pos) => { if (!node.isInline) { return; @@ -91,8 +92,10 @@ export const addMarkStep = ({ state, tr, step, newTr, map, doc, user, date }) => step.to, // Math.min(step.to, pos + node.nodeSize), newFormatMark, ); + meta.formatMark = newFormatMark; meta.step = step; + newTr.setMeta(TrackChangesBasePluginKey, meta); } else if (formatChangeMark) { newTr.removeMark(Math.max(step.from, pos), Math.min(step.to, pos + node.nodeSize), formatChangeMark); diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/removeMarkStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/removeMarkStep.js index 832cc23c53..8b88a75228 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/removeMarkStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/removeMarkStep.js @@ -3,6 +3,7 @@ import { Mapping, RemoveMarkStep } from 'prosemirror-transform'; import { Node } from 'prosemirror-model'; import { v4 as uuidv4 } from 'uuid'; import { TrackDeleteMarkName, TrackFormatMarkName } from '../constants.js'; +import { TrackChangesBasePluginKey } from '../plugins/trackChangesBasePlugin.js'; /** * Remove mark step. @@ -16,6 +17,8 @@ import { TrackDeleteMarkName, TrackFormatMarkName } from '../constants.js'; * @param {string} options.date Date. */ export const removeMarkStep = ({ state, tr, step, newTr, map, doc, user, date }) => { + const meta = {}; + doc.nodesBetween(step.from, step.to, (node, pos) => { if (!node.isInline) { return true; @@ -62,18 +65,25 @@ export const removeMarkStep = ({ state, tr, step, newTr, map, doc, user, date }) } if (after.length || before.length) { + const newFormatMark = state.schema.marks[TrackFormatMarkName].create({ + id: uuidv4(), + author: user.name, + authorEmail: user.email, + date, + before, + after, + }); + newTr.addMark( Math.max(step.from, pos), Math.min(step.to, pos + node.nodeSize), - state.schema.marks[TrackFormatMarkName].create({ - id: uuidv4(), - author: user.name, - authorEmail: user.email, - date, - before, - after, - }), + newFormatMark, ); + + meta.formatMark = newFormatMark; + meta.step = step; + + newTr.setMeta(TrackChangesBasePluginKey, meta); } else if (formatChangeMark) { newTr.removeMark(Math.max(step.from, pos), Math.min(step.to, pos + node.nodeSize), formatChangeMark); } diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceStep.js index 88b9d1501b..78d4571e41 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceStep.js @@ -61,9 +61,10 @@ export const replaceStep = ({ state, tr, step, newTr, map, doc, user, date, orig const mirrorIndex = map.maps.length - 1; map.appendMap(condensedStep.getMap(), mirrorIndex); - // Prepare meta for the transaction - meta.insertedMark = insertedMark; - meta.step = condensedStep; + if (newStep.from !== mappedNewStepTo) { + meta.insertedMark = insertedMark; + meta.step = condensedStep; + } if (!newTr.selection.eq(trTemp.selection)) { newTr.setSelection(trTemp.selection); @@ -78,8 +79,10 @@ export const replaceStep = ({ state, tr, step, newTr, map, doc, user, date, orig user, date, }); + meta.deletionNodes = deletionNodes; meta.deletionMark = deletionMark; + map.appendMapping(deletionMap); } diff --git a/packages/superdoc/src/stores/comments-store.js b/packages/superdoc/src/stores/comments-store.js index e2b1e7d499..3ff7edb69a 100644 --- a/packages/superdoc/src/stores/comments-store.js +++ b/packages/superdoc/src/stores/comments-store.js @@ -130,18 +130,22 @@ export const useCommentsStore = defineStore('comments', () => { } }); - // If this is a new tracked change, add it to our comments if (event === 'add') { + // If this is a new tracked change, add it to our comments addComment({ superdoc, comment }); - } - - // If we have an update event, simply update the composable comment - else if (event === 'update') { + } else if (event === 'update') { + // If we have an update event, simply update the composable comment const existingTrackedChange = commentsList.value.find( (comment) => comment.commentId === changeId ); if (!existingTrackedChange) return; + existingTrackedChange.trackedChangeText = trackedChangeText; + + if (deletedText) { + existingTrackedChange.deletedText = deletedText; + } + const emitData = { type: COMMENT_EVENTS.UPDATE, comment: existingTrackedChange.getValues(), @@ -474,6 +478,7 @@ export const useCommentsStore = defineStore('comments', () => { trackedChange: comment.trackedChange || false, trackedChangeText: comment.trackedChangeText, trackedChangeType: comment.trackedChangeType, + deletedText: comment.trackedDeletedText, }); addComment({ superdoc, comment: newComment });