I’m a bit of beginner in prosemirror ,let me explain my project first,its a tiptap editor with lot of custom nodes,that tiptap json in rendered into html by custom renderers (NoteRenderer,ImageRenderer,etc..) and the tiptap json structure is in a custom way.when user goes into edit mode,the Editor instance is rendered and onupdate,each renderer has a callback that updates the json according to the custom json structure.on the Editor im doing something like this (code is a mixture from AI garbage,github(trackchangesExtesnion)by some guy and myself)
onUpdate: ({ editor, transaction }) => {
try {
// Skip if this transaction was generated by our code
if (transaction.getMeta('isOurReAddTransaction')) {
console.log('Skipping our transaction');
return;
}
if (!transaction.docChanged) {
return;
}
// if this is a redo or undo, ignore it.
if (transaction.getMeta('history$')) {
return;
}
// check if it is synced by another client or the server, need to ignore too
const syncMeta = transaction.getMeta('y-sync$');
if (syncMeta && syncMeta.isChangeOrigin) {
return;
}
// has no real step
if (!transaction.steps.length) {
return;
}
// Extract valid steps from transaction
const allSteps = transaction.steps.map((step) =>
Step.fromJSON(editor.state.doc.type.schema, step.toJSON())
);
// Track cursor position and offset
const currentCursorPos = editor.state.selection.from;
let cursorOffset = 0;
let isReplace = false;
// First pass: calculate offsets and identify replacements
allSteps.forEach((step: Step, _index: number, _arr: Step[]) => {
if (step instanceof ReplaceStep) {
let delCount = 0;
if (step.from !== step.to) {
const slice = transaction.docs[_index].slice(step.from, step.to);
slice.content.forEach((node) => {
const isInsertNode = node.marks.find((m) => m.type.name === MARK_INSERTION);
if (!isInsertNode) {
delCount += node.nodeSize;
}
});
}
cursorOffset += delCount;
const newCount = step.slice ? step.slice.size : 0;
if (newCount && delCount) {
isReplace = true;
}
}
});
if (!isReplace) {
cursorOffset = 0;
}
// Create a single transaction for all our changes
let tr = editor.state.tr;
tr.setMeta('isOurReAddTransaction', true);
// Handle insertions and deletions in one pass
let reAddOffset = 0;
// Process insertion steps
allSteps.forEach((step: Step, index: number) => {
if (step instanceof ReplaceStep && step.slice?.content?.size > 0) {
const from = step.from + reAddOffset;
const to = step.from + reAddOffset + step.slice.size;
// Add mark for inserted content
tr = tr.addMark(from, to, editor.schema.marks.insertion.create());
tr = tr.removeMark(from, to, editor.schema.marks.deletion);
}
});
// Process deletion steps
allSteps.forEach((step: Step, index: number) => {
if (step instanceof ReplaceStep && step.from !== step.to) {
const invertedStep = step.invert(transaction.docs[index]);
// Calculate offset for re-insertion point
const reAddFrom = invertedStep.from + reAddOffset;
// Create a list to track nodes we need to skip
const nodesToSkip: { from: number; to: number }[] = [];
// Function to identify nodes with insertion marks
const findInsertionMarks = (content: Fragment, parentOffset: number) => {
content.forEach((node, offset: number) => {
const start = parentOffset + offset;
const end = start + node.nodeSize;
if (node.content && node.content.size) {
// This node has children, traverse them
findInsertionMarks(node.content, start);
} else {
// Check for insertion mark
if (
node.marks.find(
(m: { type: { name: string } }) => m.type.name === MARK_INSERTION
)
) {
nodesToSkip.push({ from: start, to: end });
}
}
});
};
// Find nodes to skip
findInsertionMarks(invertedStep.slice.content, invertedStep.from);
// First, re-add the deleted content
tr = tr.replace(reAddFrom, reAddFrom, invertedStep.slice);
// Add highlight to the re-added content
const reAddTo = reAddFrom + invertedStep.slice.size;
tr = tr.addMark(reAddFrom, reAddTo, editor.schema.marks.deletion.create());
// Process nodes that need to be skipped (removed again)
// We need to adjust positions based on where content was re-added
let skipOffset = 0;
nodesToSkip.sort((a, b) => b.from - a.from); // Process from end to start to avoid position shifts
nodesToSkip.forEach(({ from, to }) => {
// Calculate position in the current document
// Adjust from/to by:
// 1. Subtract original position (invertedStep.from)
// 2. Add new insertion position (reAddFrom)
// 3. Subtract any offset from previous removals
const skipFrom = from - invertedStep.from + reAddFrom - skipOffset;
const skipTo = to - invertedStep.from + reAddFrom - skipOffset;
// Remove this node
tr = tr.delete(skipFrom, skipTo);
// Update offset for future skip operations
skipOffset += skipTo - skipFrom;
});
console.log('nodesToSkip', nodesToSkip);
// Update the offset for future operations - account for nodes we removed
reAddOffset += invertedStep.slice.size - skipOffset;
// Position cursor at the end of the re-added content, adjusted for removed nodes
const finalCursorPos = reAddTo - skipOffset;
tr = tr.setSelection(TextSelection.create(tr.doc, finalCursorPos));
}
});
// If we made any changes, apply them
if (tr.steps.length > 0) {
editor.view.dispatch(tr);
return;
}
content.set(editor.getJSON());
// Update content (object reference instead of using set method)
} catch (e) {
console.error(e);
}
}
initially the onUpdate was like
onUpdate: ({ editor, transaction }) => {
try {
const editorJson = editor.getJSON();
editorJson.content = filterContent(editorJson.content); //filter empty nodes
if (editorJson.content?.length === 0 && isParent) {
editorJson.content.push({
type: 'paragraph',
content: [
{
type: 'heading',
text: '',
attrs: {
level: 1,
componentUUID: uuidv4()
}
}
]
});
}
onUpdate(editorJson, editor, transaction.selection); //**this will trigger the respective node's onupdate function**
} catch (e) {
console.error('Error in track changes:', e);
}
}
now there are some bugs 1.When i enter into edit mode with lot of custom nodes in it,it goes on a recursive render 2.when i type something in custom node(note),new note node is inserted for everychange
can you please give me insights about tackling these,but my ultimate goal is to visualize diff between 2 versions of document(not as json,but in visually),or is there any better way?