a6ff7623db
The editor lagged while typing (worse with doc size, and under collaboration the same cost is paid for every REMOTE keystroke). ProseMirror itself was fine — the overhead was the surrounding work done on every transaction. Behavior is 1:1; only WHEN work runs changed. - getJSON() off the keystroke path: `onUpdate` no longer serializes the whole doc synchronously — the serialization now runs inside a 3s debounce (new hook use-page-content-cache.ts), flushed on unmount so the last snapshot isn't lost. - footnote numbering: merged 3 per-docChanged O(n) doc walks into one, and short-circuit the whole-doc renumber when the doc has no footnotes and the transaction didn't insert one (step-slice scan — covers typing/paste/collab). - toolbar: replaced per-keystroke `editor.can().undo()/.redo()` dry-runs with cheap history-depth reads (Yjs undoManager stack length / pm-history depth). - render side-effect bug: `remote.attach()` moved out of the render body into a useEffect. - debounced the TOC all-headings rescan and memoized the slash-command suggestion build (was rebuilt twice per keystroke). - node menus (image/video/audio/pdf/callout/subpages): the per-transaction selectors early-return a cheap isActive check instead of running getAttributes + multiple alignment probes while their node type is inactive (shouldShow still controls display — appears exactly when it did). - code blocks: the global selectionUpdate listener is now added only for mermaid blocks (the only consumer of the selected state), eliminating N listeners + N setStates per caret move for normal code blocks. Deferred (documented, collab hot-path risk): full conditional menu MOUNTING (menu-less-frame risk on same-tx context switch) and code-block re-tokenization debounce / language-persist (self-dispatching meta tx + node-attr writes interact with collab/undo). The route split from #342 already keeps lowlight off startup. Gate: editor-ext build + 252/252 tests, client editor tests pass, tsc --noEmit 0, client build ok. New tests: footnote no-footnote-doc → 0 traversals + numbering unchanged; page-content-cache onUpdate-no-sync-getJSON + flush-on-unmount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import type { Editor } from "@tiptap/react";
|
|
import { useEditorState } from "@tiptap/react";
|
|
import { undoDepth, redoDepth } from "@tiptap/pm/history";
|
|
import { yUndoPluginKey } from "@tiptap/y-tiptap";
|
|
|
|
export interface ToolbarState {
|
|
isBold: boolean;
|
|
isItalic: boolean;
|
|
isUnderline: boolean;
|
|
isStrike: boolean;
|
|
isCode: boolean;
|
|
isSubscript: boolean;
|
|
isSuperscript: boolean;
|
|
isBulletList: boolean;
|
|
isOrderedList: boolean;
|
|
isTaskList: boolean;
|
|
canUndo: boolean;
|
|
canRedo: boolean;
|
|
}
|
|
|
|
// Undo/redo availability, computed WITHOUT `editor.can().undo()/.redo()`.
|
|
//
|
|
// `editor.can()` runs the command as a dry-run (building a throwaway state +
|
|
// transaction) — the most expensive work in this selector, and it ran on every
|
|
// keystroke (and every REMOTE keystroke under collaboration). Instead we read
|
|
// the history stack depth directly, which is a cheap plugin-state lookup and
|
|
// mirrors exactly what the undo/redo commands themselves check:
|
|
//
|
|
// - Collaboration (Yjs): the yjs UndoManager's undo/redo stack lengths — the
|
|
// same `undoStack.length === 0` / `redoStack.length === 0` guard the
|
|
// Collaboration extension's undo/redo commands use.
|
|
// - Plain history (templates / non-collab): prosemirror-history's undoDepth /
|
|
// redoDepth, which back the UndoRedo extension.
|
|
//
|
|
// When neither history backend is installed (the pre-sync static editor —
|
|
// mainExtensions only, undoRedo disabled), both fall through to 0 -> false,
|
|
// matching the previous `safeCan` behavior.
|
|
function historyAvailability(editor: Editor): {
|
|
canUndo: boolean;
|
|
canRedo: boolean;
|
|
} {
|
|
const state = editor.state;
|
|
|
|
// Collaboration history (Yjs) takes precedence when present.
|
|
const yState = yUndoPluginKey.getState(state) as
|
|
| { undoManager?: { undoStack: unknown[]; redoStack: unknown[] } }
|
|
| undefined;
|
|
if (yState?.undoManager) {
|
|
return {
|
|
canUndo: yState.undoManager.undoStack.length > 0,
|
|
canRedo: yState.undoManager.redoStack.length > 0,
|
|
};
|
|
}
|
|
|
|
// Plain prosemirror-history (returns 0 when the history plugin is absent).
|
|
return {
|
|
canUndo: undoDepth(state) > 0,
|
|
canRedo: redoDepth(state) > 0,
|
|
};
|
|
}
|
|
|
|
export function useToolbarState(editor: Editor | null): ToolbarState | null {
|
|
return useEditorState({
|
|
editor,
|
|
selector: (ctx) => {
|
|
if (!ctx.editor) return null;
|
|
const { canUndo, canRedo } = historyAvailability(ctx.editor);
|
|
return {
|
|
isBold: ctx.editor.isActive("bold"),
|
|
isItalic: ctx.editor.isActive("italic"),
|
|
isUnderline: ctx.editor.isActive("underline"),
|
|
isStrike: ctx.editor.isActive("strike"),
|
|
isCode: ctx.editor.isActive("code"),
|
|
isSubscript: ctx.editor.isActive("subscript"),
|
|
isSuperscript: ctx.editor.isActive("superscript"),
|
|
isBulletList: ctx.editor.isActive("bulletList"),
|
|
isOrderedList: ctx.editor.isActive("orderedList"),
|
|
isTaskList: ctx.editor.isActive("taskList"),
|
|
canUndo,
|
|
canRedo,
|
|
};
|
|
},
|
|
});
|
|
}
|