mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
fix(editor): isolate undo history per chapter
Key the editor by chapter id so each chapter gets a fresh TipTap instance with its own history. Previously a single instance was reused across chapters, so the content swap entered the undo stack and Ctrl+Z after switching restored the prior chapter's doc into the current one, which autosave then persisted. With remount, save the cursor/scroll position on unmount (from a ref, not the editor, which is destroyed by then) and load it on mount, replacing the in-place chapter-switch bookkeeping. Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
This commit is contained in:
1 parent
92941a2758
commit
c0da4c53db
2 files changed
+13
-17
No files matched your search
@@ -263,6 +263,7 @@ export function EditorView() {
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<Editor
|
<Editor
|
||||||
|
key={chapter.id}
|
||||||
bookId={book.id}
|
bookId={book.id}
|
||||||
chapterId={chapter.id}
|
chapterId={chapter.id}
|
||||||
content={chapter.content}
|
content={chapter.content}
|
||||||
|
|||||||
+12
-17
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { EditorContent, useEditor, type Editor as TiptapEditor } from "@tiptap/react";
|
import { EditorContent, useEditor, type Editor as TiptapEditor } from "@tiptap/react";
|
||||||
import type { JSONContent } from "@tiptap/core";
|
import type { JSONContent } from "@tiptap/core";
|
||||||
import { editorExtensions } from "./extensions";
|
import { editorExtensions } from "./extensions";
|
||||||
import { loadPosition, savePosition } from "./positions";
|
import { loadPosition, savePosition, type ChapterPosition } from "./positions";
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -16,7 +16,7 @@ export function Editor({ bookId, chapterId, content, onChange, onReady }: Editor
|
|||||||
const onChangeRef = useRef(onChange);
|
const onChangeRef = useRef(onChange);
|
||||||
onChangeRef.current = onChange;
|
onChangeRef.current = onChange;
|
||||||
|
|
||||||
const openChapter = useRef(chapterId);
|
const latest = useRef<ChapterPosition | null>(null);
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: editorExtensions,
|
extensions: editorExtensions,
|
||||||
@@ -28,6 +28,7 @@ export function Editor({ bookId, chapterId, content, onChange, onReady }: Editor
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onReady(editor);
|
onReady(editor);
|
||||||
|
return () => onReady(null);
|
||||||
}, [editor, onReady]);
|
}, [editor, onReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,15 +36,6 @@ export function Editor({ bookId, chapterId, content, onChange, onReady }: Editor
|
|||||||
let active = true;
|
let active = true;
|
||||||
const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null;
|
const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null;
|
||||||
|
|
||||||
const previous = openChapter.current;
|
|
||||||
if (previous !== chapterId) {
|
|
||||||
const { from, to } = editor.state.selection;
|
|
||||||
savePosition(bookId, previous, { from, to, scroll: scroller?.scrollTop ?? 0 });
|
|
||||||
}
|
|
||||||
openChapter.current = chapterId;
|
|
||||||
|
|
||||||
editor.commands.setContent(content, { emitUpdate: false });
|
|
||||||
|
|
||||||
const saved = loadPosition(bookId, chapterId);
|
const saved = loadPosition(bookId, chapterId);
|
||||||
const size = editor.state.doc.content.size;
|
const size = editor.state.doc.content.size;
|
||||||
const selection = saved ? { from: Math.min(saved.from, size), to: Math.min(saved.to, size) } : 0;
|
const selection = saved ? { from: Math.min(saved.from, size), to: Math.min(saved.to, size) } : 0;
|
||||||
@@ -63,18 +55,20 @@ export function Editor({ bookId, chapterId, content, onChange, onReady }: Editor
|
|||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [chapterId, editor, bookId]);
|
}, [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null;
|
const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null;
|
||||||
let timer: ReturnType<typeof setTimeout>;
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
|
const capture = () => {
|
||||||
|
const { from, to } = editor.state.selection;
|
||||||
|
latest.current = { from, to, scroll: scroller?.scrollTop ?? 0 };
|
||||||
|
};
|
||||||
const persist = () => {
|
const persist = () => {
|
||||||
|
capture();
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => savePosition(bookId, chapterId, latest.current!), 400);
|
||||||
const { from, to } = editor.state.selection;
|
|
||||||
savePosition(bookId, openChapter.current, { from, to, scroll: scroller?.scrollTop ?? 0 });
|
|
||||||
}, 400);
|
|
||||||
};
|
};
|
||||||
editor.on("selectionUpdate", persist);
|
editor.on("selectionUpdate", persist);
|
||||||
scroller?.addEventListener("scroll", persist, { passive: true });
|
scroller?.addEventListener("scroll", persist, { passive: true });
|
||||||
@@ -82,8 +76,9 @@ export function Editor({ bookId, chapterId, content, onChange, onReady }: Editor
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
editor.off("selectionUpdate", persist);
|
editor.off("selectionUpdate", persist);
|
||||||
scroller?.removeEventListener("scroll", persist);
|
scroller?.removeEventListener("scroll", persist);
|
||||||
|
if (latest.current) savePosition(bookId, chapterId, latest.current);
|
||||||
};
|
};
|
||||||
}, [editor, bookId]);
|
}, [editor, bookId, chapterId]);
|
||||||
|
|
||||||
return <EditorContent editor={editor} className="editor-host" />;
|
return <EditorContent editor={editor} className="editor-host" />;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user