mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-03 11:37:03 +00:00
fix keyboard fixed
This commit is contained in:
1 parent
20365dd02d
commit
a31f99cb76
12 files changed
+321
-83
No files matched your search
+89
-31
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { EditorContent, useEditor, type Editor as TiptapEditor } from "@tiptap/react";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import { editorExtensions } from "./extensions";
|
||||
import { loadPosition, savePosition, type ChapterPosition } from "./positions";
|
||||
import { useBook } from "../store/useBook";
|
||||
|
||||
interface EditorProps {
|
||||
bookId: string;
|
||||
@@ -13,13 +15,34 @@ interface EditorProps {
|
||||
onContentError: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface Cached {
|
||||
state: EditorState;
|
||||
content: JSONContent;
|
||||
scroll: number;
|
||||
}
|
||||
|
||||
function buildState(editor: TiptapEditor, content: JSONContent, onError: (error: Error) => void): EditorState {
|
||||
const base = editor.view.state;
|
||||
try {
|
||||
const doc = editor.schema.nodeFromJSON(content);
|
||||
doc.check();
|
||||
return EditorState.create({ doc, plugins: base.plugins });
|
||||
} catch (error) {
|
||||
onError(error as Error);
|
||||
return EditorState.create({ schema: base.schema, plugins: base.plugins });
|
||||
}
|
||||
}
|
||||
|
||||
export function Editor({ bookId, chapterId, content, onChange, onReady, onContentError }: EditorProps) {
|
||||
const onChangeRef = useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
const onContentErrorRef = useRef(onContentError);
|
||||
onContentErrorRef.current = onContentError;
|
||||
|
||||
const cache = useRef(new Map<string, Cached>());
|
||||
const activeId = useRef(chapterId);
|
||||
const latest = useRef<ChapterPosition | null>(null);
|
||||
const restoreToken = useRef(0);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: editorExtensions,
|
||||
@@ -31,49 +54,84 @@ export function Editor({ bookId, chapterId, content, onChange, onReady, onConten
|
||||
onUpdate: ({ editor }) => onChangeRef.current(editor.getJSON()),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onReady(editor);
|
||||
return () => onReady(null);
|
||||
}, [editor, onReady]);
|
||||
const scrollerOf = (ed: TiptapEditor) => ed.view.dom.closest(".editor-pane") as HTMLElement | null;
|
||||
|
||||
const restorePosition = (ed: TiptapEditor, id: string, focus: boolean) => {
|
||||
const scroller = scrollerOf(ed);
|
||||
const saved = loadPosition(bookId, id);
|
||||
const size = ed.state.doc.content.size;
|
||||
const selection = saved ? { from: Math.min(saved.from, size), to: Math.min(saved.to, size) } : 0;
|
||||
const chain = ed.chain().setTextSelection(selection);
|
||||
if (focus) chain.focus(undefined, { scrollIntoView: false });
|
||||
chain.run();
|
||||
if (scroller) {
|
||||
const token = ++restoreToken.current;
|
||||
const top = saved?.scroll ?? 0;
|
||||
const apply = () => {
|
||||
if (restoreToken.current === token) scroller.scrollTop = top;
|
||||
};
|
||||
apply();
|
||||
requestAnimationFrame(apply);
|
||||
document.fonts?.ready.then(apply).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const stash = (ed: TiptapEditor, id: string) => {
|
||||
const scroller = scrollerOf(ed);
|
||||
const { from, to } = ed.state.selection;
|
||||
const scroll = scroller?.scrollTop ?? 0;
|
||||
const stored = useBook.getState().book?.chapters.find((c) => c.id === id)?.content ?? cache.current.get(id)?.content ?? ed.getJSON();
|
||||
cache.current.set(id, { state: ed.view.state, content: stored, scroll });
|
||||
savePosition(bookId, id, { from, to, scroll });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
let active = true;
|
||||
const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null;
|
||||
|
||||
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;
|
||||
editor.chain().setTextSelection(selection).focus(undefined, { scrollIntoView: false }).run();
|
||||
|
||||
if (scroller) {
|
||||
const top = saved?.scroll ?? 0;
|
||||
const restore = () => {
|
||||
if (active) scroller.scrollTop = top;
|
||||
};
|
||||
restore();
|
||||
requestAnimationFrame(restore);
|
||||
document.fonts?.ready.then(restore).catch(() => {});
|
||||
}
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
cache.current.set(chapterId, { state: editor.view.state, content, scroll: 0 });
|
||||
activeId.current = chapterId;
|
||||
restorePosition(editor, chapterId, true);
|
||||
onReady(editor);
|
||||
return () => onReady(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editor]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!editor) return;
|
||||
const prev = activeId.current;
|
||||
if (prev === chapterId) return;
|
||||
|
||||
stash(editor, prev);
|
||||
const entry = cache.current.get(chapterId);
|
||||
if (entry && entry.content === content) {
|
||||
editor.view.updateState(entry.state);
|
||||
const scroller = scrollerOf(editor);
|
||||
if (scroller) scroller.scrollTop = entry.scroll;
|
||||
++restoreToken.current;
|
||||
} else {
|
||||
editor.view.updateState(buildState(editor, content, onContentErrorRef.current));
|
||||
cache.current.set(chapterId, { state: editor.view.state, content, scroll: 0 });
|
||||
restorePosition(editor, chapterId, false);
|
||||
}
|
||||
activeId.current = chapterId;
|
||||
latest.current = null;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chapterId, content, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const scroller = editor.view.dom.closest(".editor-pane") as HTMLElement | null;
|
||||
const scroller = scrollerOf(editor);
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const capture = () => {
|
||||
const { from, to } = editor.state.selection;
|
||||
latest.current = { from, to, scroll: scroller?.scrollTop ?? 0 };
|
||||
const scroll = scroller?.scrollTop ?? 0;
|
||||
latest.current = { from, to, scroll };
|
||||
const entry = cache.current.get(activeId.current);
|
||||
if (entry) entry.scroll = scroll;
|
||||
};
|
||||
const persist = () => {
|
||||
capture();
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => savePosition(bookId, chapterId, latest.current!), 400);
|
||||
timer = setTimeout(() => savePosition(bookId, activeId.current, latest.current!), 400);
|
||||
};
|
||||
editor.on("selectionUpdate", persist);
|
||||
scroller?.addEventListener("scroll", persist, { passive: true });
|
||||
@@ -81,9 +139,9 @@ export function Editor({ bookId, chapterId, content, onChange, onReady, onConten
|
||||
clearTimeout(timer);
|
||||
editor.off("selectionUpdate", persist);
|
||||
scroller?.removeEventListener("scroll", persist);
|
||||
if (latest.current) savePosition(bookId, chapterId, latest.current);
|
||||
if (latest.current) savePosition(bookId, activeId.current, latest.current);
|
||||
};
|
||||
}, [editor, bookId, chapterId]);
|
||||
}, [editor, bookId]);
|
||||
|
||||
return <EditorContent editor={editor} className="editor-host" />;
|
||||
}
|
||||
Reference in new issue
Block a user