editor: improve math block node

* fix exit on arrow up
* allow entering the node block via up/down
* live math rendering

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-12-12 16:29:37 +05:00
committed by Abdullah Atta
parent e9d6cad534
commit d743147837
4 changed files with 86 additions and 57 deletions

View File

@@ -22,7 +22,7 @@ import { useEffect, useRef } from "react";
import { Button } from "../../components/button";
import { useTimer } from "../../hooks/use-timer";
import { SelectionBasedReactNodeViewProps } from "../react/types";
import { MathBlock, MathBlockAttributes } from "./math-block";
import { MathBlockAttributes } from "./math-block";
import { loadKatex } from "./plugin/renderers/katex";
import { useThemeEngineStore } from "@notesnook/theme";
import SimpleBar from "simplebar-react";
@@ -32,34 +32,37 @@ export function MathBlockComponent(
props: SelectionBasedReactNodeViewProps<MathBlockAttributes>
) {
const { editor, node, forwardRef, getPos } = props;
const { indentLength, indentType, caretPosition } = node.attrs;
const { caretPosition } = node.attrs;
const isActive = editor.isActive(MathBlock.name);
const elementRef = useRef<HTMLElement>();
const codeElementRef = useRef<HTMLElement>();
const toolbarRef = useRef<HTMLDivElement>(null);
const pos = getPos();
const { from, to } = editor.state.selection;
const isActive = from >= pos && to < pos + node.nodeSize;
const mathRendererRef = useRef<HTMLElement>();
const mathInputElementRef = useRef<HTMLElement>();
const theme = useThemeEngineStore((store) => store.theme);
const { enabled, start } = useTimer(1000);
console.log("Rerendering MathBlockComponent", isActive);
useEffect(() => {
if (isActive) return;
(async function () {
const pos = getPos();
const node = editor.state.doc.nodeAt(pos);
const text = node?.textContent;
if (text && elementRef.current) {
const katex = await loadKatex();
if (mathRendererRef.current) {
if (text && text.trim()) {
const katex = await loadKatex();
elementRef.current.innerHTML = katex.renderToString(text, {
displayMode: true,
globalGroup: true,
throwOnError: false
});
mathRendererRef.current.innerHTML = katex.renderToString(text, {
displayMode: true,
globalGroup: true,
throwOnError: false
});
} else {
mathRendererRef.current.innerHTML = "";
}
}
})();
}, [isActive]);
}, [isActive, node.textContent]);
return (
<>
@@ -68,7 +71,7 @@ export function MathBlockComponent(
flexDirection: "column",
borderRadius: "default",
overflow: "hidden",
...(isActive ? {} : { height: "1px", width: 0, visibility: "hidden" })
display: isActive ? "flex" : "none"
}}
>
<SimpleBar
@@ -79,7 +82,7 @@ export function MathBlockComponent(
<div>
<Text
ref={(ref) => {
codeElementRef.current = ref ?? undefined;
mathInputElementRef.current = ref ?? undefined;
forwardRef?.(ref);
}}
autoCorrect="off"
@@ -89,7 +92,6 @@ export function MathBlockComponent(
pre: {
fontFamily: "inherit !important",
tabSize: "inherit !important",
// background: "transparent !important",
padding: "10px !important",
margin: "0px !important",
width: "100%",
@@ -105,13 +107,10 @@ export function MathBlockComponent(
}
},
fontFamily: "monospace",
whiteSpace: "pre", // TODO !important
whiteSpace: "pre",
tabSize: 1,
position: "relative",
lineHeight: "20px",
// bg: "var(--background-secondary)",
// color: "white",
// overflowX: "hidden",
display: "flex"
}}
spellCheck={false}
@@ -119,7 +118,6 @@ export function MathBlockComponent(
</div>
</SimpleBar>
<Flex
ref={toolbarRef}
contentEditable={false}
sx={{
bg: "var(--background-secondary)",
@@ -136,29 +134,6 @@ export function MathBlockComponent(
: ""}
</Text>
) : null}
<Button
variant={"icon"}
sx={{
p: 1,
opacity: "1 !important"
}}
title={strings.toggleIndentationMode()}
disabled={!editor.isEditable}
onClick={() => {
if (!editor.isEditable) return;
editor.commands.changeCodeBlockIndentation({
type: indentType === "space" ? "tab" : "space",
amount: indentLength
});
}}
>
<Text variant={"subBody"}>
{indentType === "space" ? strings.spaces() : strings.tabs()}:{" "}
{indentLength}
</Text>
</Button>
<Button
variant={"icon"}
sx={{
@@ -191,7 +166,7 @@ export function MathBlockComponent(
onClick={() => {
editor.storage.copyToClipboard?.(
node.textContent,
codeElementRef?.current?.innerHTML
mathInputElementRef?.current?.innerHTML
);
start();
}}
@@ -208,7 +183,18 @@ export function MathBlockComponent(
) : null}
</Flex>
</Flex>
<Box contentEditable={false} ref={elementRef} />
{node.textContent && node.textContent.trim() && (
<SimpleBar
style={{
borderRadius: "5px",
paddingInline: "5px",
border: isActive ? "1px solid var(--border)" : "none",
marginTop: isActive ? "8px" : "0px"
}}
>
<Box contentEditable={false} ref={mathRendererRef} />
</SimpleBar>
)}
</>
);
}

View File

@@ -53,9 +53,31 @@ export type MathBlockAttributes = {
caretPosition?: CaretPosition;
};
export interface MathBlockOptions {
/**
* Define whether the node should be exited on triple enter.
* Defaults to `true`.
*/
exitOnTripleEnter: boolean;
/**
* Define whether the node should be exited on arrow down if there is no node after it.
* Defaults to `true`.
*/
exitOnArrowDown: boolean;
/**
* Define whether the node should be exited on arrow up if there is no node before it.
* Defaults to `true`.
*/
exitOnArrowUp: boolean;
/**
* Custom HTML attributes that should be added to the rendered HTML tag.
*/
HTMLAttributes: Record<string, unknown>;
}
// simple inputrule for block math
const REGEX_BLOCK_MATH_DOLLARS = /\$\$\$\s+$/; //new RegExp("\$\$\s+$", "i");
export const MathBlock = Node.create({
export const MathBlock = Node.create<MathBlockOptions>({
name: "mathBlock",
group: "block math",
content: "text*", // important!
@@ -64,6 +86,15 @@ export const MathBlock = Node.create({
draggable: false,
marks: "",
addOptions() {
return {
exitOnTripleEnter: true,
exitOnArrowDown: true,
exitOnArrowUp: true,
HTMLAttributes: {}
};
},
addAttributes() {
return {
language: {
@@ -189,7 +220,6 @@ export const MathBlock = Node.create({
if (indentation) return indentOnEnter(editor, $from, indentation);
return false;
},
// exit node on arrow up
ArrowUp: ({ editor }) => {
if (!this.options.exitOnArrowUp) {
@@ -198,18 +228,29 @@ export const MathBlock = Node.create({
const { state } = editor;
const { selection } = state;
const { $anchor, empty } = selection;
const { $anchor, empty, $from } = selection;
if (!empty || $anchor.parent.type !== this.type) {
return false;
}
const isAtStart = $anchor.pos === 1;
if (!isAtStart) {
const isAtStartOfNode = $from.parentOffset === 0;
if (!isAtStartOfNode) {
return false;
}
return editor.commands.insertContentAt(0, "<p></p>");
const before = $from.before();
if (before === undefined) {
return false;
}
const nodeBefore = state.doc.nodeAt(before);
if (nodeBefore) {
editor.commands.setNodeSelection($from.before());
return false;
}
return editor.commands.exitCode();
},
// exit node on arrow down
ArrowDown: ({ editor }) => {
@@ -359,7 +400,7 @@ export const MathBlock = Node.create({
addNodeView() {
return createSelectionBasedNodeView(MathBlockComponent, {
contentDOMFactory: () => {
const content = document.createElement("div");
const content = document.createElement("pre");
content.classList.add("node-content-wrapper");
content.style.whiteSpace = "pre";
// caret is not visible if content element width is 0px

View File

@@ -347,6 +347,7 @@ const useTiptap = (
CodeBlock.name,
Table.name,
Blockquote.name,
MathBlock.name,
...LIST_NODE_TYPES
]
}),

View File

@@ -132,7 +132,8 @@
.ProseMirror > div.codeblock-view-content-wrap,
.ProseMirror > div.taskList-view-content-wrap,
.ProseMirror > div.math-block.math-node {
.ProseMirror > div.math-block.math-node,
.ProseMirror > div.mathBlock-view-content-wrap {
margin-top: 1em;
margin-bottom: 1em;
}