From 26d5df5b505e2fa7ce344bce02b837c424c49b8c Mon Sep 17 00:00:00 2001
From: jkcs <1778768609@qq.com>
Date: Thu, 22 Aug 2024 09:52:22 +0800
Subject: [PATCH 1/3] inline equation
---
.../05-equation/.bnexample.json | 11 +
examples/06-custom-schema/05-equation/App.tsx | 90 ++++++
.../06-custom-schema/05-equation/Equation.tsx | 292 ++++++++++++++++++
.../06-custom-schema/05-equation/README.md | 11 +
.../06-custom-schema/05-equation/index.html | 14 +
.../06-custom-schema/05-equation/main.tsx | 11 +
.../06-custom-schema/05-equation/package.json | 40 +++
.../06-custom-schema/05-equation/styles.css | 62 ++++
.../05-equation/tsconfig.json | 36 +++
.../05-equation/vite.config.ts | 32 ++
package-lock.json | 9 +-
.../core/src/schema/inlineContent/types.ts | 6 +
.../src/schema/ReactInlineContentSpec.tsx | 13 +-
playground/package.json | 2 +
playground/src/examples.gen.tsx | 27 ++
15 files changed, 650 insertions(+), 6 deletions(-)
create mode 100644 examples/06-custom-schema/05-equation/.bnexample.json
create mode 100644 examples/06-custom-schema/05-equation/App.tsx
create mode 100644 examples/06-custom-schema/05-equation/Equation.tsx
create mode 100644 examples/06-custom-schema/05-equation/README.md
create mode 100644 examples/06-custom-schema/05-equation/index.html
create mode 100644 examples/06-custom-schema/05-equation/main.tsx
create mode 100644 examples/06-custom-schema/05-equation/package.json
create mode 100644 examples/06-custom-schema/05-equation/styles.css
create mode 100644 examples/06-custom-schema/05-equation/tsconfig.json
create mode 100644 examples/06-custom-schema/05-equation/vite.config.ts
diff --git a/examples/06-custom-schema/05-equation/.bnexample.json b/examples/06-custom-schema/05-equation/.bnexample.json
new file mode 100644
index 000000000..308c3fc66
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "jkcs",
+ "tags": ["Equation", "Inline Equation", "Custom Schemas", "Latex", "Slash Menu"],
+ "dependencies": {
+ "katex": "^0.16.11",
+ "@types/katex": "^0.16.7",
+ "react-icons": "^5.2.1"
+ }
+}
diff --git a/examples/06-custom-schema/05-equation/App.tsx b/examples/06-custom-schema/05-equation/App.tsx
new file mode 100644
index 000000000..9bdb684d9
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/App.tsx
@@ -0,0 +1,90 @@
+import {
+ BlockNoteSchema,
+ defaultInlineContentSpecs,
+ filterSuggestionItems,
+} from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import {
+ SuggestionMenuController,
+ getDefaultReactSlashMenuItems,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import { RiFormula } from "react-icons/ri";
+import "@blocknote/mantine/style.css";
+
+import { InlineEquation } from "./Equation";
+
+// Our schema with block specs, which contain the configs and implementations for blocks
+// that we want our editor to use.
+const schema = BlockNoteSchema.create({
+ inlineContentSpecs: {
+ ...defaultInlineContentSpecs,
+ inlineEquation: InlineEquation,
+ },
+});
+
+const insertInlineEquation = (editor: typeof schema.BlockNoteEditor) => ({
+ icon: ,
+ title: "Inline Equation",
+ key: "inlineEquation",
+ subtext: "Insert mathematical symbols in text.",
+ aliases: ["equation", "latex", "katex"],
+ group: "Other",
+ onItemClick: () => {
+ editor.insertInlineContent([
+ {
+ type: "inlineEquation",
+ },
+ " ", // add a space after the mention
+ ]);
+ },
+});
+
+export default function App() {
+ const editor = useCreateBlockNote({
+ schema,
+ initialContent: [
+ {
+ type: "paragraph",
+ content: [
+ "This is an example inline equation ",
+ {
+ type: "inlineEquation",
+ props: {
+ content: "c = \\pm\\sqrt{a^2 + b^2}",
+ },
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: "Press the '/' key to open the Slash Menu and add another",
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+ filterSuggestionItems(
+ [
+ ...getDefaultReactSlashMenuItems(editor),
+ insertInlineEquation(editor),
+ ],
+ query
+ )
+ }
+ />
+
+ );
+}
diff --git a/examples/06-custom-schema/05-equation/Equation.tsx b/examples/06-custom-schema/05-equation/Equation.tsx
new file mode 100644
index 000000000..7efe7668a
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/Equation.tsx
@@ -0,0 +1,292 @@
+import {
+ createReactInlineContentSpec,
+ useBlockNoteEditor,
+ useComponentsContext,
+ useEditorContentOrSelectionChange,
+} from "@blocknote/react";
+import { NodeViewWrapper } from "@tiptap/react";
+import {
+ ChangeEvent,
+ forwardRef,
+ MouseEvent as ReactMouseEvent,
+ TextareaHTMLAttributes,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import katex from "katex";
+import { AiOutlineEnter } from "react-icons/ai";
+import "katex/dist/katex.min.css";
+import "./styles.css";
+import { Node as TipTapNode } from "@tiptap/pm/model";
+
+const TextareaView = forwardRef<
+ HTMLTextAreaElement,
+ {
+ autofocus?: boolean;
+ } & TextareaHTMLAttributes
+>((props, ref) => {
+ const { autofocus, ...rest } = props;
+ useEffect(() => {
+ if (autofocus && ref && typeof ref !== "function" && ref.current) {
+ ref.current.setSelectionRange(0, ref.current.value.length);
+ ref.current.focus();
+ }
+ }, [autofocus, ref]);
+
+ return (
+
+ );
+});
+
+export const InlineEquationView = (props: { node: TipTapNode }) => {
+ const content = props.node.attrs.content;
+ const nodeSize = props.node.nodeSize;
+ const textareaRef = useRef(null);
+ const contentRef = useRef(null);
+ const containerRef = useRef(null);
+ const [focus, setFocus] = useState(!content);
+ const [curEdge, setCurEdge] = useState(!content);
+ const Components = useComponentsContext()!;
+ const editor = useBlockNoteEditor();
+ const html = useMemo(
+ () =>
+ katex.renderToString(content, {
+ throwOnError: false,
+ }),
+ [content]
+ );
+
+ const getTextareaEdge = () => {
+ const $textarea = textareaRef.current;
+ if (!$textarea) {
+ return {};
+ }
+
+ return {
+ isLeftEdge:
+ $textarea.selectionStart === 0 && $textarea.selectionEnd === 0,
+ isRightEdge:
+ $textarea.selectionStart === $textarea.value.length &&
+ $textarea.selectionEnd === $textarea.value.length,
+ };
+ };
+
+ const getPos = useCallback((): number => {
+ let position = 0;
+
+ editor._tiptapEditor.state.doc.descendants(
+ (node: TipTapNode, pos: number) => {
+ if (node === props.node) {
+ position = pos;
+ return false;
+ }
+ }
+ );
+
+ return position;
+ }, [editor, props.node]);
+
+ useEditorContentOrSelectionChange(() => {
+ const pos = getPos();
+ const courPos = editor._tiptapEditor.state.selection.from;
+ const selection = editor.getSelection();
+
+ setCurEdge(!selection && (courPos === pos + nodeSize || courPos === pos));
+ });
+
+ useEffect(() => {
+ if (focus) {
+ contentRef.current?.click();
+ }
+ }, [focus]);
+
+ const handleEnter = useCallback(
+ (event: ReactMouseEvent | KeyboardEvent) => {
+ event.preventDefault();
+ const pos = getPos();
+ if (!content) {
+ const node = props.node;
+ const view = editor._tiptapEditor.view;
+
+ const tr = view.state.tr.delete(pos, pos + node.nodeSize);
+
+ view.dispatch(tr);
+ editor._tiptapEditor.commands.setTextSelection(pos);
+ } else {
+ editor._tiptapEditor.commands.setTextSelection(pos + nodeSize);
+ }
+ editor.focus();
+ setFocus(false);
+ setCurEdge(true);
+ },
+ [content, editor, getPos, nodeSize, props.node]
+ );
+
+ const handleMenuNavigationKeys = useCallback(
+ (event: KeyboardEvent) => {
+ const textareaEdge = getTextareaEdge();
+ const pos = getPos();
+ const courPos = editor._tiptapEditor.state.selection.from;
+
+ if (event.key === "ArrowLeft") {
+ if (courPos === pos + nodeSize && !focus) {
+ setFocus(true);
+ }
+ if (textareaEdge.isLeftEdge) {
+ event.preventDefault();
+ editor.focus();
+ editor._tiptapEditor.commands.setTextSelection(pos);
+ setFocus(false);
+ }
+ return true;
+ }
+
+ if (event.key === "ArrowRight") {
+ if (courPos === pos && !focus) {
+ setFocus(true);
+ }
+ if (textareaEdge.isRightEdge) {
+ event.preventDefault();
+ editor.focus();
+ editor._tiptapEditor.commands.setTextSelection(pos + nodeSize);
+ setFocus(false);
+ }
+ return true;
+ }
+
+ if (event.key === "Enter" && focus) {
+ handleEnter(event);
+ return true;
+ }
+
+ return false;
+ },
+ [editor, focus, getPos, handleEnter, nodeSize]
+ );
+
+ useEffect(() => {
+ const domEle = editor._tiptapEditor?.view?.dom;
+ if (focus || curEdge) {
+ domEle?.addEventListener("keydown", handleMenuNavigationKeys, true);
+ }
+
+ return () => {
+ domEle?.removeEventListener("keydown", handleMenuNavigationKeys, true);
+ };
+ }, [editor, focus, handleMenuNavigationKeys, curEdge]);
+
+ const handleChange = (e: ChangeEvent) => {
+ const val = e.target.value;
+ const pos = getPos();
+ const node = props.node;
+ const view = editor._tiptapEditor.view;
+
+ const tr = view.state.tr.replaceWith(
+ pos,
+ pos + node.nodeSize,
+ view.state.schema.nodes.inlineEquation.create(
+ {
+ ...node.attrs,
+ content: val || "",
+ },
+ null
+ )
+ );
+
+ view.dispatch(tr);
+ setFocus(true);
+ };
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ containerRef.current &&
+ !containerRef.current.contains(event.target as Node)
+ ) {
+ setFocus(false);
+ }
+ };
+
+ document.addEventListener("pointerup", handleClickOutside, true);
+ return () => {
+ document.removeEventListener("pointerup", handleClickOutside, true);
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {!content ? (
+ setFocus(true)} className={"equation-empty"}>
+ New Equation
+
+ ) : (
+ setFocus(true)}
+ className={"equation-content"}
+ dangerouslySetInnerHTML={{ __html: html }}>
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export const InlineEquation = createReactInlineContentSpec(
+ {
+ type: "inlineEquation",
+ propSchema: {
+ content: {
+ default: "",
+ },
+ },
+ content: "none",
+ // copy content
+ renderHTML: (props) => {
+ const { HTMLAttributes, node } = props;
+ const dom = document.createElement("span");
+ dom.setAttribute("data-inline-content-type", "inlineEquation");
+ Object.keys(HTMLAttributes).forEach((key) => {
+ dom.setAttribute(key, HTMLAttributes[key]);
+ });
+ dom.innerText = node.attrs.content;
+
+ return { dom };
+ },
+ },
+ {
+ render: (props) => {
+ return ;
+ },
+ }
+);
diff --git a/examples/06-custom-schema/05-equation/README.md b/examples/06-custom-schema/05-equation/README.md
new file mode 100644
index 000000000..2e140b566
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/README.md
@@ -0,0 +1,11 @@
+# Inline Equation
+
+In this example, we create a custom `Inline Equation`
+
+**Try it out:** Press the "/" key to open the Slash Menu and insert an `Equation` block!
+
+**Relevant Docs:**
+
+- [Custom Blocks](/docs/custom-schemas/custom-blocks)
+- [Changing Slash Menu Items](/docs/ui-components/suggestion-menus#changing-slash-menu-items)
+- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
diff --git a/examples/06-custom-schema/05-equation/index.html b/examples/06-custom-schema/05-equation/index.html
new file mode 100644
index 000000000..772555bb2
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Inline Equation
+
+
+
+
+
+
diff --git a/examples/06-custom-schema/05-equation/main.tsx b/examples/06-custom-schema/05-equation/main.tsx
new file mode 100644
index 000000000..f88b490fb
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/06-custom-schema/05-equation/package.json b/examples/06-custom-schema/05-equation/package.json
new file mode 100644
index 000000000..4751728ce
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@blocknote/example-equation",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --max-warnings 0"
+ },
+ "dependencies": {
+ "@blocknote/core": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/ariakit": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/shadcn": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "katex": "^0.16.11",
+ "@types/katex": "^0.16.7",
+ "react-icons": "^5.2.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^4.3.1",
+ "eslint": "^8.10.0",
+ "vite": "^5.3.4"
+ },
+ "eslintConfig": {
+ "extends": [
+ "../../../.eslintrc.js"
+ ]
+ },
+ "eslintIgnore": [
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/examples/06-custom-schema/05-equation/styles.css b/examples/06-custom-schema/05-equation/styles.css
new file mode 100644
index 000000000..b1793b3e9
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/styles.css
@@ -0,0 +1,62 @@
+.equation-content {
+ caret-color: rgb(55, 53, 47);
+ padding: 2px 2px;
+ border-radius: 4px;
+ transform: translate3d(-4px, 0, 0);
+ margin-right: -4px;
+ white-space: pre;
+}
+
+
+.equation.focus .equation-content {
+ background: rgba(37, 135, 231, 0.12);
+}
+
+.equation .equation-empty {
+ white-space: nowrap;
+ font-size: 12px;
+ background: rgb(207, 207, 207);
+ caret-color: rgb(55, 53, 47);
+ vertical-align: top;
+ padding: 2px 4px;
+ border-radius: 4px;
+ transform: translate3d(-4px, 0, 0);
+ margin-right: -8px;
+}
+
+.equation-label {
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ padding: 4px;
+}
+
+.equation-textarea {
+ min-width: 200px;
+ margin-right: 10px;
+ outline: none;
+ border: none;
+ font-size: 14px;
+}
+
+.equation-enter {
+ user-select: none;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ white-space: nowrap;
+ height: 28px;
+ border-radius: 4px;
+ box-shadow: rgba(15, 15, 15, 0.1) 0 0 0 1px inset, rgba(15, 15, 15, 0.1) 0 1px 2px;
+ background: rgb(35, 131, 226);
+ color: white;
+ fill: white;
+ line-height: 1.2;
+ padding-left: 12px;
+ padding-right: 12px;
+ font-size: 14px;
+ font-weight: 500;
+ align-self: flex-start;
+}
\ No newline at end of file
diff --git a/examples/06-custom-schema/05-equation/tsconfig.json b/examples/06-custom-schema/05-equation/tsconfig.json
new file mode 100644
index 000000000..1bd8ab3c5
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/06-custom-schema/05-equation/vite.config.ts b/examples/06-custom-schema/05-equation/vite.config.ts
new file mode 100644
index 000000000..f62ab20bc
--- /dev/null
+++ b/examples/06-custom-schema/05-equation/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/package-lock.json b/package-lock.json
index e3d7c46a7..9848e8f35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17763,13 +17763,14 @@
"dev": true
},
"node_modules/katex": {
- "version": "0.16.10",
- "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
- "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
+ "version": "0.16.11",
+ "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.11.tgz",
+ "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
+ "license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
@@ -28840,6 +28841,7 @@
"@mantine/core": "^7.10.1",
"@mui/icons-material": "^5.16.1",
"@mui/material": "^5.16.1",
+ "@types/katex": "^0.16.7",
"@uppy/core": "^3.13.1",
"@uppy/dashboard": "^3.9.1",
"@uppy/drag-drop": "^3.1.1",
@@ -28851,6 +28853,7 @@
"@uppy/status-bar": "^3.1.1",
"@uppy/webcam": "^3.4.2",
"@uppy/xhr-upload": "^3.4.0",
+ "katex": "^0.16.11",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts
index 5ef62f593..c8d5036e3 100644
--- a/packages/core/src/schema/inlineContent/types.ts
+++ b/packages/core/src/schema/inlineContent/types.ts
@@ -1,11 +1,17 @@
import { Node } from "@tiptap/core";
import { PropSchema, Props } from "../propTypes";
import { StyleSchema, Styles } from "../styles/types";
+import { Node as ProseMirrorNode } from "prosemirror-model";
+import { DOMOutputSpec } from "@tiptap/pm/model";
export type CustomInlineContentConfig = {
type: string;
content: "styled" | "none"; // | "plain"
readonly propSchema: PropSchema;
+ renderHTML?: (props: {
+ node: ProseMirrorNode;
+ HTMLAttributes: Record;
+ }) => DOMOutputSpec;
// content: "inline" | "none" | "table";
};
// InlineContentConfig contains the "schema" info about an InlineContent type
diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx
index 27999dcfe..5027b7371 100644
--- a/packages/react/src/schema/ReactInlineContentSpec.tsx
+++ b/packages/react/src/schema/ReactInlineContentSpec.tsx
@@ -23,6 +23,7 @@ import {
// import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView";
import { FC } from "react";
import { renderToDOMSpec } from "./@util/ReactRenderUtil";
+import { Node } from "@tiptap/pm/model";
// this file is mostly analogoues to `customBlocks.ts`, but for React blocks
@@ -35,6 +36,7 @@ export type ReactInlineContentImplementation<
render: FC<{
inlineContent: InlineContentFromConfig;
contentRef: (node: HTMLElement | null) => void;
+ node: Node;
}>;
// TODO?
// toExternalHTML?: FC<{
@@ -109,7 +111,10 @@ export function createReactInlineContentSpec<
return getInlineContentParseRules(inlineContentConfig);
},
- renderHTML({ node }) {
+ renderHTML({ node, ...args }) {
+ if (inlineContentConfig.renderHTML) {
+ return inlineContentConfig.renderHTML({ node, ...args });
+ }
const editor = this.options.editor;
const ic = nodeToCustomInlineContent(
@@ -119,7 +124,9 @@ export function createReactInlineContentSpec<
) as any as InlineContentFromConfig; // TODO: fix cast
const Content = inlineContentImplementation.render;
const output = renderToDOMSpec(
- (refCB) => ,
+ (refCB) => (
+
+ ),
editor
);
@@ -131,7 +138,6 @@ export function createReactInlineContentSpec<
);
},
- // TODO: needed?
addNodeView() {
const editor = this.options.editor;
return (props) =>
@@ -148,6 +154,7 @@ export function createReactInlineContentSpec<
propSchema={inlineContentConfig.propSchema}>
Date: Thu, 22 Aug 2024 10:12:48 +0800
Subject: [PATCH 2/3] fix: npm lint
---
packages/react/package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/react/package.json b/packages/react/package.json
index b7b27abce..f229ba690 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -55,6 +55,7 @@
"@floating-ui/react": "^0.26.4",
"@tiptap/core": "^2.5.0",
"@tiptap/react": "^2.5.0",
+ "@tiptap/pm": "^2.5.0",
"lodash.merge": "^4.6.2",
"react": "^18",
"react-dom": "^18",
From 519f34a00aaa767f3686a59ababbd2bcb53fec9d Mon Sep 17 00:00:00 2001
From: yousefed
Date: Mon, 26 Aug 2024 14:01:00 +0200
Subject: [PATCH 3/3] implement isSelected
---
.../05-equation/.bnexample.json | 2 +-
examples/06-custom-schema/05-equation/App.tsx | 4 +-
.../06-custom-schema/05-equation/Equation.tsx | 106 ++++++------------
.../06-custom-schema/05-equation/README.md | 2 +-
.../06-custom-schema/05-equation/styles.css | 92 +++++++--------
.../core/src/schema/inlineContent/types.ts | 4 +-
packages/mantine/src/popover/Popover.tsx | 5 +-
.../react/src/editor/ComponentsContext.tsx | 3 +-
.../src/schema/ReactInlineContentSpec.tsx | 20 +++-
playground/src/examples.gen.tsx | 2 +-
10 files changed, 107 insertions(+), 133 deletions(-)
diff --git a/examples/06-custom-schema/05-equation/.bnexample.json b/examples/06-custom-schema/05-equation/.bnexample.json
index 308c3fc66..896230e29 100644
--- a/examples/06-custom-schema/05-equation/.bnexample.json
+++ b/examples/06-custom-schema/05-equation/.bnexample.json
@@ -2,7 +2,7 @@
"playground": true,
"docs": false,
"author": "jkcs",
- "tags": ["Equation", "Inline Equation", "Custom Schemas", "Latex", "Slash Menu"],
+ "tags": ["Equation", "Inline Equation", "Custom Schemas", "Latex", "Katex"],
"dependencies": {
"katex": "^0.16.11",
"@types/katex": "^0.16.7",
diff --git a/examples/06-custom-schema/05-equation/App.tsx b/examples/06-custom-schema/05-equation/App.tsx
index 9bdb684d9..f5c7c4215 100644
--- a/examples/06-custom-schema/05-equation/App.tsx
+++ b/examples/06-custom-schema/05-equation/App.tsx
@@ -4,14 +4,14 @@ import {
filterSuggestionItems,
} from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
import {
SuggestionMenuController,
getDefaultReactSlashMenuItems,
useCreateBlockNote,
} from "@blocknote/react";
-import { BlockNoteView } from "@blocknote/mantine";
import { RiFormula } from "react-icons/ri";
-import "@blocknote/mantine/style.css";
import { InlineEquation } from "./Equation";
diff --git a/examples/06-custom-schema/05-equation/Equation.tsx b/examples/06-custom-schema/05-equation/Equation.tsx
index 7efe7668a..512efa369 100644
--- a/examples/06-custom-schema/05-equation/Equation.tsx
+++ b/examples/06-custom-schema/05-equation/Equation.tsx
@@ -1,26 +1,25 @@
+import { InlineContentFromConfig } from "@blocknote/core";
import {
createReactInlineContentSpec,
useBlockNoteEditor,
useComponentsContext,
- useEditorContentOrSelectionChange,
} from "@blocknote/react";
+import { Node as TipTapNode } from "@tiptap/pm/model";
import { NodeViewWrapper } from "@tiptap/react";
+import katex from "katex";
+import "katex/dist/katex.min.css";
import {
ChangeEvent,
- forwardRef,
MouseEvent as ReactMouseEvent,
TextareaHTMLAttributes,
+ forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
- useState,
} from "react";
-import katex from "katex";
import { AiOutlineEnter } from "react-icons/ai";
-import "katex/dist/katex.min.css";
import "./styles.css";
-import { Node as TipTapNode } from "@tiptap/pm/model";
const TextareaView = forwardRef<
HTMLTextAreaElement,
@@ -47,14 +46,17 @@ const TextareaView = forwardRef<
);
});
-export const InlineEquationView = (props: { node: TipTapNode }) => {
- const content = props.node.attrs.content;
+export const InlineEquationView = (props: {
+ inlineContent: InlineContentFromConfig;
+ node: TipTapNode;
+ isSelected: boolean;
+}) => {
+ const content = props.inlineContent.props.content;
const nodeSize = props.node.nodeSize;
const textareaRef = useRef(null);
const contentRef = useRef(null);
const containerRef = useRef(null);
- const [focus, setFocus] = useState(!content);
- const [curEdge, setCurEdge] = useState(!content);
+
const Components = useComponentsContext()!;
const editor = useBlockNoteEditor();
const html = useMemo(
@@ -95,25 +97,12 @@ export const InlineEquationView = (props: { node: TipTapNode }) => {
return position;
}, [editor, props.node]);
- useEditorContentOrSelectionChange(() => {
- const pos = getPos();
- const courPos = editor._tiptapEditor.state.selection.from;
- const selection = editor.getSelection();
-
- setCurEdge(!selection && (courPos === pos + nodeSize || courPos === pos));
- });
-
- useEffect(() => {
- if (focus) {
- contentRef.current?.click();
- }
- }, [focus]);
-
const handleEnter = useCallback(
- (event: ReactMouseEvent | KeyboardEvent) => {
+ (event: ReactMouseEvent | React.KeyboardEvent) => {
event.preventDefault();
const pos = getPos();
if (!content) {
+ // TODO: implement BlockNote API to easily delete inline content
const node = props.node;
const view = editor._tiptapEditor.view;
@@ -122,68 +111,50 @@ export const InlineEquationView = (props: { node: TipTapNode }) => {
view.dispatch(tr);
editor._tiptapEditor.commands.setTextSelection(pos);
} else {
+ // TODO: implement BlockNote API to easily update cursor position
editor._tiptapEditor.commands.setTextSelection(pos + nodeSize);
}
editor.focus();
- setFocus(false);
- setCurEdge(true);
},
[content, editor, getPos, nodeSize, props.node]
);
const handleMenuNavigationKeys = useCallback(
- (event: KeyboardEvent) => {
+ (event: React.KeyboardEvent) => {
const textareaEdge = getTextareaEdge();
const pos = getPos();
- const courPos = editor._tiptapEditor.state.selection.from;
if (event.key === "ArrowLeft") {
- if (courPos === pos + nodeSize && !focus) {
- setFocus(true);
- }
if (textareaEdge.isLeftEdge) {
+ // TODO: implement BlockNote API to set cursor position
event.preventDefault();
editor.focus();
editor._tiptapEditor.commands.setTextSelection(pos);
- setFocus(false);
}
return true;
}
if (event.key === "ArrowRight") {
- if (courPos === pos && !focus) {
- setFocus(true);
- }
if (textareaEdge.isRightEdge) {
+ // TODO: implement BlockNote API to set cursor position
event.preventDefault();
editor.focus();
editor._tiptapEditor.commands.setTextSelection(pos + nodeSize);
- setFocus(false);
}
return true;
}
- if (event.key === "Enter" && focus) {
+ if (event.key === "Enter" && props.isSelected) {
handleEnter(event);
return true;
}
return false;
},
- [editor, focus, getPos, handleEnter, nodeSize]
+ [editor, getPos, handleEnter, nodeSize, props.isSelected]
);
- useEffect(() => {
- const domEle = editor._tiptapEditor?.view?.dom;
- if (focus || curEdge) {
- domEle?.addEventListener("keydown", handleMenuNavigationKeys, true);
- }
-
- return () => {
- domEle?.removeEventListener("keydown", handleMenuNavigationKeys, true);
- };
- }, [editor, focus, handleMenuNavigationKeys, curEdge]);
-
+ // TODO: implement BlockNote API to easily update inline content
const handleChange = (e: ChangeEvent) => {
const val = e.target.value;
const pos = getPos();
@@ -203,39 +174,19 @@ export const InlineEquationView = (props: { node: TipTapNode }) => {
);
view.dispatch(tr);
- setFocus(true);
};
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- containerRef.current &&
- !containerRef.current.contains(event.target as Node)
- ) {
- setFocus(false);
- }
- };
-
- document.addEventListener("pointerup", handleClickOutside, true);
- return () => {
- document.removeEventListener("pointerup", handleClickOutside, true);
- };
- }, []);
-
return (
-
+
{!content ? (
- setFocus(true)} className={"equation-empty"}>
- New Equation
-
+ New Equation
) : (
setFocus(true)}
className={"equation-content"}
dangerouslySetInnerHTML={{ __html: html }}>
)}
@@ -251,6 +202,7 @@ export const InlineEquationView = (props: { node: TipTapNode }) => {
autofocus
value={content}
onChange={handleChange}
+ onKeyDown={handleMenuNavigationKeys}
/>
@@ -286,7 +238,13 @@ export const InlineEquation = createReactInlineContentSpec(
},
{
render: (props) => {
- return ;
+ return (
+
+ );
},
}
);
diff --git a/examples/06-custom-schema/05-equation/README.md b/examples/06-custom-schema/05-equation/README.md
index 2e140b566..1d40f6f5a 100644
--- a/examples/06-custom-schema/05-equation/README.md
+++ b/examples/06-custom-schema/05-equation/README.md
@@ -8,4 +8,4 @@ In this example, we create a custom `Inline Equation`
- [Custom Blocks](/docs/custom-schemas/custom-blocks)
- [Changing Slash Menu Items](/docs/ui-components/suggestion-menus#changing-slash-menu-items)
-- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
+- [Editor Setup](/docs/editor-basics/setup)
diff --git a/examples/06-custom-schema/05-equation/styles.css b/examples/06-custom-schema/05-equation/styles.css
index b1793b3e9..3507e3df3 100644
--- a/examples/06-custom-schema/05-equation/styles.css
+++ b/examples/06-custom-schema/05-equation/styles.css
@@ -1,62 +1,62 @@
.equation-content {
- caret-color: rgb(55, 53, 47);
- padding: 2px 2px;
- border-radius: 4px;
- transform: translate3d(-4px, 0, 0);
- margin-right: -4px;
- white-space: pre;
+ caret-color: rgb(55, 53, 47);
+ padding: 2px 2px;
+ border-radius: 4px;
+ transform: translate3d(-4px, 0, 0);
+ margin-right: -4px;
+ white-space: pre;
}
-
.equation.focus .equation-content {
- background: rgba(37, 135, 231, 0.12);
+ background: rgba(37, 135, 231, 0.12);
}
.equation .equation-empty {
- white-space: nowrap;
- font-size: 12px;
- background: rgb(207, 207, 207);
- caret-color: rgb(55, 53, 47);
- vertical-align: top;
- padding: 2px 4px;
- border-radius: 4px;
- transform: translate3d(-4px, 0, 0);
- margin-right: -8px;
+ white-space: nowrap;
+ font-size: 12px;
+ background: rgb(207, 207, 207);
+ caret-color: rgb(55, 53, 47);
+ vertical-align: top;
+ padding: 2px 4px;
+ border-radius: 4px;
+ transform: translate3d(-4px, 0, 0);
+ margin-right: -8px;
}
.equation-label {
- display: flex;
- align-items: flex-start;
- justify-content: flex-start;
- padding: 4px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ padding: 4px;
}
.equation-textarea {
- min-width: 200px;
- margin-right: 10px;
- outline: none;
- border: none;
- font-size: 14px;
+ min-width: 200px;
+ margin-right: 10px;
+ outline: none;
+ border: none;
+ font-size: 14px;
}
.equation-enter {
- user-select: none;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- white-space: nowrap;
- height: 28px;
- border-radius: 4px;
- box-shadow: rgba(15, 15, 15, 0.1) 0 0 0 1px inset, rgba(15, 15, 15, 0.1) 0 1px 2px;
- background: rgb(35, 131, 226);
- color: white;
- fill: white;
- line-height: 1.2;
- padding-left: 12px;
- padding-right: 12px;
- font-size: 14px;
- font-weight: 500;
- align-self: flex-start;
-}
\ No newline at end of file
+ user-select: none;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ white-space: nowrap;
+ height: 28px;
+ border-radius: 4px;
+ box-shadow: rgba(15, 15, 15, 0.1) 0 0 0 1px inset,
+ rgba(15, 15, 15, 0.1) 0 1px 2px;
+ background: rgb(35, 131, 226);
+ color: white;
+ fill: white;
+ line-height: 1.2;
+ padding-left: 12px;
+ padding-right: 12px;
+ font-size: 14px;
+ font-weight: 500;
+ align-self: flex-start;
+}
diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts
index c8d5036e3..708bab857 100644
--- a/packages/core/src/schema/inlineContent/types.ts
+++ b/packages/core/src/schema/inlineContent/types.ts
@@ -1,8 +1,8 @@
import { Node } from "@tiptap/core";
+import { DOMOutputSpec } from "@tiptap/pm/model";
+import { Node as ProseMirrorNode } from "prosemirror-model";
import { PropSchema, Props } from "../propTypes";
import { StyleSchema, Styles } from "../styles/types";
-import { Node as ProseMirrorNode } from "prosemirror-model";
-import { DOMOutputSpec } from "@tiptap/pm/model";
export type CustomInlineContentConfig = {
type: string;
diff --git a/packages/mantine/src/popover/Popover.tsx b/packages/mantine/src/popover/Popover.tsx
index b81f842e6..28af93afa 100644
--- a/packages/mantine/src/popover/Popover.tsx
+++ b/packages/mantine/src/popover/Popover.tsx
@@ -11,7 +11,7 @@ import { forwardRef } from "react";
export const Popover = (
props: ComponentProps["Generic"]["Popover"]["Root"]
) => {
- const { children, opened, position, ...rest } = props;
+ const { children, opened, onChange, position, ...rest } = props;
assertEmpty(rest);
@@ -20,7 +20,8 @@ export const Popover = (
withinPortal={false}
zIndex={10000}
opened={opened}
- position={position}>
+ position={position}
+ onChange={onChange}>
{children}
);
diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx
index 7a46df454..24d6861ab 100644
--- a/packages/react/src/editor/ComponentsContext.tsx
+++ b/packages/react/src/editor/ComponentsContext.tsx
@@ -9,8 +9,8 @@ import {
useContext,
} from "react";
-import { DefaultReactSuggestionItem } from "../components/SuggestionMenu/types";
import { DefaultReactGridSuggestionItem } from "../components/SuggestionMenu/GridSuggestionMenu/types";
+import { DefaultReactSuggestionItem } from "../components/SuggestionMenu/types";
export type ComponentProps = {
FormattingToolbar: {
@@ -241,6 +241,7 @@ export type ComponentProps = {
children?: ReactNode;
opened?: boolean;
position?: "top" | "right" | "bottom" | "left";
+ onChange?: (open: boolean) => void;
};
Content: {
className?: string;
diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx
index 5027b7371..68243236a 100644
--- a/packages/react/src/schema/ReactInlineContentSpec.tsx
+++ b/packages/react/src/schema/ReactInlineContentSpec.tsx
@@ -21,9 +21,9 @@ import {
ReactNodeViewRenderer,
} from "@tiptap/react";
// import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView";
+import { Node } from "@tiptap/pm/model";
import { FC } from "react";
import { renderToDOMSpec } from "./@util/ReactRenderUtil";
-import { Node } from "@tiptap/pm/model";
// this file is mostly analogoues to `customBlocks.ts`, but for React blocks
@@ -37,6 +37,7 @@ export type ReactInlineContentImplementation<
inlineContent: InlineContentFromConfig;
contentRef: (node: HTMLElement | null) => void;
node: Node;
+ isSelected: boolean;
}>;
// TODO?
// toExternalHTML?: FC<{
@@ -93,7 +94,7 @@ export function createReactInlineContentSpec<
name: inlineContentConfig.type as T["type"],
inline: true,
group: "inline",
- selectable: inlineContentConfig.content === "styled",
+ selectable: true, //inlineContentConfig.content === "styled",
atom: inlineContentConfig.content === "none",
content: (inlineContentConfig.content === "styled"
? "inline*"
@@ -125,7 +126,12 @@ export function createReactInlineContentSpec<
const Content = inlineContentImplementation.render;
const output = renderToDOMSpec(
(refCB) => (
-
+
),
editor
);
@@ -147,6 +153,13 @@ export function createReactInlineContentSpec<
const ref = (NodeViewContent({}) as any).ref;
const Content = inlineContentImplementation.render;
+
+ const isSelected =
+ props.selected &&
+ props.editor.state.selection.from === props.getPos() &&
+ props.editor.state.selection.to ===
+ props.getPos() + props.node.nodeSize;
+
return (
}
@@ -162,6 +175,7 @@ export function createReactInlineContentSpec<
editor.schema.styleSchema
) as any as InlineContentFromConfig // TODO: fix cast
}
+ isSelected={isSelected}
/>
);
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index 89c3852ae..c8eb80b95 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -899,7 +899,7 @@
"Inline Equation",
"Custom Schemas",
"Latex",
- "Slash Menu"
+ "Katex"
],
"dependencies": {
"katex": "^0.16.11",