Fix undo & redo for documents (#314)

This commit is contained in:
Hakan Shehu
2026-02-02 09:38:00 +01:00
committed by GitHub
parent 5ea69ba06d
commit 6d40a2b574
2 changed files with 105 additions and 70 deletions

View File

@@ -27,7 +27,7 @@ export class YDoc {
constructor(state?: Uint8Array | string | Uint8Array[] | string[]) {
this.doc = new Y.Doc();
this.undoManager = new Y.UndoManager(this.doc, {
this.undoManager = new Y.UndoManager(this.doc.getMap('object'), {
trackedOrigins: new Set([ORIGIN]),
});

View File

@@ -7,7 +7,7 @@ import {
useEditor,
} from '@tiptap/react';
import { debounce, isEqual } from 'lodash-es';
import { Fragment, useCallback, useEffect, useMemo, useRef } from 'react';
import { Fragment, useEffect, useMemo, useRef } from 'react';
import { toast } from 'sonner';
import {
@@ -107,6 +107,84 @@ const buildYDoc = (
return ydoc;
};
interface UndoRedoParams {
editor: ReturnType<typeof useEditor>;
ydoc: YDoc;
nodeId: string;
userId: string;
}
const performUndo = async ({
editor,
ydoc,
nodeId,
userId,
}: UndoRedoParams) => {
const beforeContent = ydoc.getObject<RichTextContent>();
const update = ydoc.undo();
if (!update) {
return;
}
const afterContent = ydoc.getObject<RichTextContent>();
if (isEqual(beforeContent, afterContent)) {
return;
}
const editorContent = buildEditorContent(nodeId, afterContent);
editor.chain().setContent(editorContent).run();
const result = await window.colanode.executeMutation({
type: 'document.update',
userId,
documentId: nodeId,
update: encodeState(update),
});
if (!result.success) {
toast.error(result.error.message);
}
};
const performRedo = async ({
editor,
ydoc,
nodeId,
userId,
}: UndoRedoParams) => {
const beforeContent = ydoc.getObject<RichTextContent>();
console.log('beforeContent', beforeContent);
const update = ydoc.redo();
console.log('afterContent', ydoc.getObject<RichTextContent>());
console.log('update', update);
if (!update) {
return;
}
const afterContent = ydoc.getObject<RichTextContent>();
if (isEqual(beforeContent, afterContent)) {
return;
}
const editorContent = buildEditorContent(nodeId, afterContent);
editor.chain().setContent(editorContent).run();
const result = await window.colanode.executeMutation({
type: 'document.update',
userId,
documentId: nodeId,
update: encodeState(update),
});
if (!result.success) {
toast.error(result.error.message);
}
};
export const DocumentEditor = ({
node,
state,
@@ -119,6 +197,7 @@ export const DocumentEditor = ({
const hasPendingChanges = useRef(false);
const revisionRef = useRef(state?.revision ?? 0);
const ydocRef = useRef<YDoc>(buildYDoc(state, updates));
const editorRef = useRef<ReturnType<typeof useEditor>>(null);
const debouncedSave = useMemo(
() =>
@@ -257,19 +336,38 @@ export const DocumentEditor = ({
spellCheck: 'false',
},
handleKeyDown: (_, event) => {
if (!editorRef.current) {
return false;
}
if (event.key === 'z' && event.metaKey && !event.shiftKey) {
event.preventDefault();
undo();
performUndo({
editor: editorRef.current,
ydoc: ydocRef.current,
nodeId: node.id,
userId: workspace.userId,
});
return true;
}
if (event.key === 'z' && event.metaKey && event.shiftKey) {
event.preventDefault();
redo();
performRedo({
editor: editorRef.current,
ydoc: ydocRef.current,
nodeId: node.id,
userId: workspace.userId,
});
return true;
}
if (event.key === 'y' && event.metaKey) {
event.preventDefault();
redo();
performRedo({
editor: editorRef.current,
ydoc: ydocRef.current,
nodeId: node.id,
userId: workspace.userId,
});
return true;
}
},
@@ -332,71 +430,8 @@ export const DocumentEditor = ({
}
}, [state, updates, editor]);
const undo = useCallback(async () => {
if (!editor) {
return;
}
const beforeContent = ydocRef.current.getObject<RichTextContent>();
const update = ydocRef.current.undo();
if (!update) {
return;
}
const afterContent = ydocRef.current.getObject<RichTextContent>();
if (isEqual(beforeContent, afterContent)) {
return;
}
const editorContent = buildEditorContent(node.id, afterContent);
editor.chain().setContent(editorContent).run();
const result = await window.colanode.executeMutation({
type: 'document.update',
userId: workspace.userId,
documentId: node.id,
update: encodeState(update),
});
if (!result.success) {
toast.error(result.error.message);
}
}, [node.id, editor]);
const redo = useCallback(async () => {
if (!editor) {
return;
}
const beforeContent = ydocRef.current.getObject<RichTextContent>();
const update = ydocRef.current.redo();
if (!update) {
return;
}
const afterContent = ydocRef.current.getObject<RichTextContent>();
if (isEqual(beforeContent, afterContent)) {
return;
}
const editorContent = buildEditorContent(node.id, afterContent);
editor.chain().setContent(editorContent).run();
const result = await window.colanode.executeMutation({
type: 'document.update',
userId: workspace.userId,
documentId: node.id,
update: encodeState(update),
});
if (!result.success) {
toast.error(result.error.message);
}
}, [node.id, editor]);
// Keep editorRef updated so handleKeyDown can access the current editor
editorRef.current = editor;
return (
<>