mirror of
https://github.com/colanode/colanode.git
synced 2026-02-24 11:59:53 +01:00
Fix undo & redo for documents (#314)
This commit is contained in:
@@ -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]),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user