({
lowlight: this.options.lowlight,
defaultLanguage: this.options.defaultLanguage,
}),
+ // Mermaid hides its when unselected, so the browser's native
+ // vertical caret movement skips past it. Land the cursor inside the
+ // source explicitly.
+ new Plugin({
+ props: {
+ handleKeyDown: (view, event) => {
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
+ return false;
+ }
+ const { state } = view;
+ const { selection } = state;
+ if (
+ !selection.empty ||
+ !(selection instanceof TextSelection)
+ ) {
+ return false;
+ }
+ const { $from } = selection;
+ if ($from.depth === 0 || $from.parent.type === codeBlockType) {
+ return false;
+ }
+ const dir = event.key === 'ArrowUp' ? 'up' : 'down';
+ if (!view.endOfTextblock(dir)) return false;
+
+ const isMermaid = (node: any) =>
+ node?.type === codeBlockType && node.attrs.language === 'mermaid';
+
+ if (event.key === 'ArrowUp') {
+ if ($from.parentOffset !== 0) return false;
+ const beforePos = $from.before();
+ const prev = state.doc.resolve(beforePos).nodeBefore;
+ if (!isMermaid(prev)) return false;
+ const endPos = beforePos - 1;
+ view.dispatch(
+ state.tr.setSelection(
+ TextSelection.create(state.doc, endPos),
+ ),
+ );
+ return true;
+ }
+ if ($from.parentOffset !== $from.parent.nodeSize - 2) return false;
+ const afterPos = $from.after();
+ const next = state.doc.resolve(afterPos).nodeAfter;
+ if (!isMermaid(next)) return false;
+ const startPos = afterPos + 1;
+ view.dispatch(
+ state.tr.setSelection(
+ TextSelection.create(state.doc, startPos),
+ ),
+ );
+ return true;
+ },
+ },
+ }),
];
},
});
diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts
index 3bfd01778..8d03577c7 100644
--- a/packages/editor-ext/src/lib/utils.ts
+++ b/packages/editor-ext/src/lib/utils.ts
@@ -338,6 +338,15 @@ export const isRowGripSelected = ({
return !!gripRow;
};
+// TipTap's `editor.view` proxy throws if accessed before mount or after destroy.
+// Guard floating-menu callbacks (getReferencedVirtualElement, shouldShow) with
+// this before touching `editor.view.nodeDOM(...)`.
+export function isEditorReady(
+ editor: Editor | null | undefined,
+): editor is Editor {
+ return !!editor && editor.isInitialized;
+}
+
export function isTextSelected(editor: Editor) {
const {
state: {