diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index bc6fea3..634e3d1 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -263,6 +263,7 @@ export function EditorView() { )} setChapterContent(chapter.id, content)} diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index bde5dac..0da1108 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -2,25 +2,20 @@ import { useEffect, useRef } from "react"; import { EditorContent, useEditor, type Editor as TiptapEditor } from "@tiptap/react"; import type { JSONContent } from "@tiptap/core"; import { editorExtensions } from "./extensions"; +import { loadPosition, savePosition } from "./positions"; interface EditorProps { + bookId: string; chapterId: string; content: JSONContent; onChange: (content: JSONContent) => void; onReady: (editor: TiptapEditor | null) => void; } -interface ChapterPosition { - from: number; - to: number; - scroll: number; -} - -export function Editor({ chapterId, content, onChange, onReady }: EditorProps) { +export function Editor({ bookId, chapterId, content, onChange, onReady }: EditorProps) { const onChangeRef = useRef(onChange); onChangeRef.current = onChange; - const positions = useRef(new Map()); const openChapter = useRef(chapterId); const editor = useEditor({ @@ -37,36 +32,58 @@ export function Editor({ chapterId, content, onChange, onReady }: EditorProps) { useEffect(() => { if (!editor) return; + let active = true; const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null; const previous = openChapter.current; - const switching = previous !== chapterId; - if (switching) { + if (previous !== chapterId) { const { from, to } = editor.state.selection; - positions.current.set(previous, { from, to, scroll: scroller?.scrollTop ?? 0 }); + savePosition(bookId, previous, { from, to, scroll: scroller?.scrollTop ?? 0 }); } openChapter.current = chapterId; editor.commands.setContent(content, { emitUpdate: false }); - const saved = positions.current.get(chapterId); + const saved = loadPosition(bookId, chapterId); const size = editor.state.doc.content.size; const selection = saved ? { from: Math.min(saved.from, size), to: Math.min(saved.to, size) } : 0; - if (switching) { - editor.chain().setTextSelection(selection).focus(undefined, { scrollIntoView: false }).run(); - } else { - editor.commands.setTextSelection(selection); - } + editor.chain().setTextSelection(selection).focus(undefined, { scrollIntoView: false }).run(); if (scroller) { const top = saved?.scroll ?? 0; - scroller.scrollTop = top; - requestAnimationFrame(() => { - scroller.scrollTop = top; - }); + const restore = () => { + if (active) scroller.scrollTop = top; + }; + restore(); + requestAnimationFrame(restore); + document.fonts?.ready.then(restore).catch(() => {}); } + + return () => { + active = false; + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chapterId, editor]); + }, [chapterId, editor, bookId]); + + useEffect(() => { + if (!editor) return; + const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null; + let timer: ReturnType; + const persist = () => { + clearTimeout(timer); + timer = setTimeout(() => { + const { from, to } = editor.state.selection; + savePosition(bookId, openChapter.current, { from, to, scroll: scroller?.scrollTop ?? 0 }); + }, 400); + }; + editor.on("selectionUpdate", persist); + scroller?.addEventListener("scroll", persist, { passive: true }); + return () => { + clearTimeout(timer); + editor.off("selectionUpdate", persist); + scroller?.removeEventListener("scroll", persist); + }; + }, [editor, bookId]); return ; } diff --git a/src/editor/positions.ts b/src/editor/positions.ts new file mode 100644 index 0000000..c804ce0 --- /dev/null +++ b/src/editor/positions.ts @@ -0,0 +1,27 @@ +export interface ChapterPosition { + from: number; + to: number; + scroll: number; +} + +const KEY = "margin-positions"; + +type Store = Record>; + +function readAll(): Store { + try { + return JSON.parse(localStorage.getItem(KEY) || "{}") as Store; + } catch { + return {}; + } +} + +export function loadPosition(bookId: string, chapterId: string): ChapterPosition | null { + return readAll()[bookId]?.[chapterId] ?? null; +} + +export function savePosition(bookId: string, chapterId: string, position: ChapterPosition): void { + const all = readAll(); + (all[bookId] ||= {})[chapterId] = position; + localStorage.setItem(KEY, JSON.stringify(all)); +}