From f102c8033b89f96abe5f9049b6a0066f3c5cf01b Mon Sep 17 00:00:00 2001 From: jkcs <1778768609@qq.com> Date: Thu, 18 Jul 2024 10:41:53 +0800 Subject: [PATCH 1/5] test equation --- .../13-equation/.bnexample.json | 11 + examples/03-ui-components/13-equation/App.tsx | 98 +++++++++ .../03-ui-components/13-equation/Equation.tsx | 206 ++++++++++++++++++ .../03-ui-components/13-equation/README.md | 11 + .../03-ui-components/13-equation/index.html | 14 ++ .../03-ui-components/13-equation/main.tsx | 11 + .../03-ui-components/13-equation/package.json | 40 ++++ .../03-ui-components/13-equation/styles.css | 74 +++++++ .../13-equation/tsconfig.json | 36 +++ .../13-equation/vite.config.ts | 32 +++ package-lock.json | 10 +- playground/package.json | 3 + playground/src/examples.gen.tsx | 27 +++ 13 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 examples/03-ui-components/13-equation/.bnexample.json create mode 100644 examples/03-ui-components/13-equation/App.tsx create mode 100644 examples/03-ui-components/13-equation/Equation.tsx create mode 100644 examples/03-ui-components/13-equation/README.md create mode 100644 examples/03-ui-components/13-equation/index.html create mode 100644 examples/03-ui-components/13-equation/main.tsx create mode 100644 examples/03-ui-components/13-equation/package.json create mode 100644 examples/03-ui-components/13-equation/styles.css create mode 100644 examples/03-ui-components/13-equation/tsconfig.json create mode 100644 examples/03-ui-components/13-equation/vite.config.ts diff --git a/examples/03-ui-components/13-equation/.bnexample.json b/examples/03-ui-components/13-equation/.bnexample.json new file mode 100644 index 0000000000..1aadbc234b --- /dev/null +++ b/examples/03-ui-components/13-equation/.bnexample.json @@ -0,0 +1,11 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", "Slash Menu"], + "dependencies": { + "katex": "^0.16.11", + "@types/katex": "^0.16.7", + "react-icons": "^5.2.1" + } +} diff --git a/examples/03-ui-components/13-equation/App.tsx b/examples/03-ui-components/13-equation/App.tsx new file mode 100644 index 0000000000..567bb52e9a --- /dev/null +++ b/examples/03-ui-components/13-equation/App.tsx @@ -0,0 +1,98 @@ +import { + BlockNoteSchema, + defaultInlineContentSpecs, + filterSuggestionItems, + insertOrUpdateBlock, +} from "@blocknote/core"; +import "@blocknote/core/fonts/inter.css"; +import { + SuggestionMenuController, + getDefaultReactSlashMenuItems, + useCreateBlockNote, +} from "@blocknote/react"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; + +import { LaTex } 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, + latex: LaTex, + }, +}); + +// Slash menu item to insert an Alert block +const insertLaTex = (editor: typeof schema.BlockNoteEditor) => ({ + title: "latex", + key: "latex", + subtext: "Used for a top-level heading", + aliases: ["latex", "heading1", "h1"], + group: "Other", + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: "paragraph", + content: [ + { + type: "latex", + props: { + open: true, + }, + content: "\\sqrt{a^2 + b^2}", + }, + ], + }); + }, +}); + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + schema, + initialContent: [ + { + type: "paragraph", + content: [ + "latex text editor ", + { + type: "latex", + content: "c = \\pm\\sqrt{a^2 + b^2}", + }, + ], + }, + { + type: "paragraph", + }, + { + type: "paragraph", + content: [ + { + type: "latex", + content: + "\\int \\frac{1}{\\sqrt{1-x^{2}}}\\mathrm{d}x= \\arcsin x +C", + }, + ], + }, + { + type: "paragraph", + }, + ], + }); + + // Renders the editor instance. + return ( + <BlockNoteView editor={editor} slashMenu={false}> + <SuggestionMenuController + triggerCharacter={"/"} + getItems={async (query: any) => + filterSuggestionItems( + [...getDefaultReactSlashMenuItems(editor), insertLaTex(editor)], + query + ) + } + /> + </BlockNoteView> + ); +} diff --git a/examples/03-ui-components/13-equation/Equation.tsx b/examples/03-ui-components/13-equation/Equation.tsx new file mode 100644 index 0000000000..8bd958ded7 --- /dev/null +++ b/examples/03-ui-components/13-equation/Equation.tsx @@ -0,0 +1,206 @@ +import { useComponentsContext } from "@blocknote/react"; +import { NodeView } from "prosemirror-view"; +import { BlockNoteEditor, propsToAttributes } from "@blocknote/core"; +import { + NodeViewProps, + NodeViewRenderer, + NodeViewWrapper, + ReactNodeViewRenderer, +} from "@tiptap/react"; +import { + createStronglyTypedTiptapNode, + createInternalInlineContentSpec, +} from "@blocknote/core"; +import { mergeAttributes } from "@tiptap/core"; +import { ChangeEvent, useEffect, useState, useRef } from "react"; +import katex from "katex"; +import "katex/dist/katex.min.css"; +import "./styles.css"; + +function LaTexView() { + const nodeView: + | ((this: { + name: string; + options: any; + storage: any; + editor: any; + type: any; + parent: any; + }) => NodeViewRenderer) + | null = function () { + const BlockContent = (props: NodeViewProps & { selectionHack: any }) => { + /* eslint-disable react-hooks/rules-of-hooks */ + const editor: BlockNoteEditor<any> = this.options.editor; + const content = props.node.textContent; + const open = props.node.attrs.open; + const textareaRef = useRef<HTMLTextAreaElement | null>(null); + const contentRef = useRef<HTMLElement | null>(null); + const [html, setHtml] = useState(""); + const [loading, setLoading] = useState(false); + const Components = useComponentsContext()!; + + useEffect(() => { + setLoading(true); + const html = katex.renderToString(content, { + throwOnError: false, + }); + setHtml(html); + setLoading(false); + }, [content]); + + useEffect(() => { + if (open) { + if (contentRef.current) { + contentRef.current.click(); + } + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current?.focus(); + textareaRef.current?.setSelectionRange( + textareaRef.current.value.length, + textareaRef.current.value.length + ); + } + }); + } + }, [open]); + + const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => { + const val = e.target.value; + const pos = props.getPos?.(); + const node = props.node; + const view = editor._tiptapEditor.view; + + const tr = view.state.tr.replaceWith( + pos, + pos + node.nodeSize, + view.state.schema.nodes.latex.create( + { + ...node.attrs, + }, + val ? view.state.schema.text(val) : null + ) + ); + + view.dispatch(tr); + }; + + return ( + <NodeViewWrapper as={"span"}> + <Components.Generic.Popover.Root> + <Components.Generic.Popover.Trigger> + <span className={"latex"} ref={contentRef}> + {loading ? ( + <span className={"latex-loading"}>latex loading...</span> + ) : ( + <span + className={"latex-content"} + dangerouslySetInnerHTML={{ __html: html }}></span> + )} + </span> + </Components.Generic.Popover.Trigger> + <Components.Generic.Popover.Content + className={"bn-popover-content bn-form-popover"} + variant={"form-popover"}> + <textarea + ref={textareaRef} + className={"latex-textarea"} + value={content} + onChange={handleChange} + /> + </Components.Generic.Popover.Content> + </Components.Generic.Popover.Root> + </NodeViewWrapper> + ); + }; + + return (props) => { + if (!(props.editor as any).contentComponent) { + return {}; + } + const ret = ReactNodeViewRenderer(BlockContent, { + stopEvent: () => true, + })(props) as NodeView; + ret.setSelection = (anchor, head) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (ret as any).renderer.updateProps({ + selectionHack: { anchor, head }, + }); + }; + + (ret as any).contentDOMElement = undefined; + + const oldUpdated = ret.update!.bind(ret); + ret.update = (node, outerDeco, innerDeco) => { + const retAsAny = ret as any; + let decorations = retAsAny.decorations; + if ( + retAsAny.decorations.decorations !== outerDeco || + retAsAny.decorations.innerDecorations !== innerDeco + ) { + decorations = { + decorations: outerDeco, + innerDecorations: innerDeco, + }; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return oldUpdated(node, decorations, undefined as any); + }; + return ret; + }; + }; + return nodeView; +} + +const propSchema = { + open: { + type: "boolean", + default: false, + }, +}; + +const node = createStronglyTypedTiptapNode({ + name: "latex", + inline: true, + group: "inline", + content: "inline*", + editable: true, + selectable: false, + + addAttributes() { + return propsToAttributes(propSchema); + }, + + parseHTML() { + return [ + { + tag: "latex", + priority: 200, + node: "latex", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "latex", + mergeAttributes(HTMLAttributes, { + "data-content-type": this.name, + }), + 0, + ]; + }, + + addNodeView: LaTexView(), +}); + +export const LaTex = createInternalInlineContentSpec( + { + content: "styled", + type: "latex", + propSchema: propSchema, + }, + { + node, + } +); diff --git a/examples/03-ui-components/13-equation/README.md b/examples/03-ui-components/13-equation/README.md new file mode 100644 index 0000000000..8c064c8b11 --- /dev/null +++ b/examples/03-ui-components/13-equation/README.md @@ -0,0 +1,11 @@ +# Alert Block + +In this example, we create a custom `Alert` block which is used to emphasize text. In addition, we create a Slash Menu item which inserts an `Alert` block. + +**Try it out:** Press the "/" key to open the Slash Menu and insert an `Alert` 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/03-ui-components/13-equation/index.html b/examples/03-ui-components/13-equation/index.html new file mode 100644 index 0000000000..6c6a363839 --- /dev/null +++ b/examples/03-ui-components/13-equation/index.html @@ -0,0 +1,14 @@ +<html lang="en"> + <head> + <script> + <!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY --> + </script> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Equation</title> + </head> + <body> + <div id="root"></div> + <script type="module" src="./main.tsx"></script> + </body> +</html> diff --git a/examples/03-ui-components/13-equation/main.tsx b/examples/03-ui-components/13-equation/main.tsx new file mode 100644 index 0000000000..f88b490fbd --- /dev/null +++ b/examples/03-ui-components/13-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( + <React.StrictMode> + <App /> + </React.StrictMode> +); diff --git a/examples/03-ui-components/13-equation/package.json b/examples/03-ui-components/13-equation/package.json new file mode 100644 index 0000000000..f1428967c0 --- /dev/null +++ b/examples/03-ui-components/13-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.0.4", + "eslint": "^8.10.0", + "vite": "^4.4.8" + }, + "eslintConfig": { + "extends": [ + "../../../.eslintrc.js" + ] + }, + "eslintIgnore": [ + "dist" + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/13-equation/styles.css b/examples/03-ui-components/13-equation/styles.css new file mode 100644 index 0000000000..7296b28dce --- /dev/null +++ b/examples/03-ui-components/13-equation/styles.css @@ -0,0 +1,74 @@ +.alert { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; + border-radius: 4px; + min-height: 48px; + padding: 4px; +} + +.alert[data-alert-type="warning"] { + background-color: #fff6e6; +} + +.alert[data-alert-type="error"] { + background-color: #ffe6e6; +} + +.alert[data-alert-type="info"] { + background-color: #e6ebff; +} + +.alert[data-alert-type="success"] { + background-color: #e6ffe6; +} + +[data-color-scheme="dark"] .alert[data-alert-type="warning"] { + background-color: #805d20; +} + +[data-color-scheme="dark"] .alert[data-alert-type="error"] { + background-color: #802020; +} + +[data-color-scheme="dark"] .alert[data-alert-type="info"] { + background-color: #203380; +} + +[data-color-scheme="dark"] .alert[data-alert-type="success"] { + background-color: #208020; +} + +.alert-icon-wrapper { + border-radius: 16px; + display: flex; + justify-content: center; + align-items: center; + margin-left: 12px; + margin-right: 12px; + height: 18px; + width: 18px; + user-select: none; + cursor: pointer; +} + +.alert-icon[data-alert-icon-type="warning"] { + color: #e69819 +} + +.alert-icon[data-alert-icon-type="error"] { + color: #d80d0d +} + +.alert-icon[data-alert-icon-type="info"] { + color: #507aff +} + +.alert-icon[data-alert-icon-type="success"] { + color: #0bc10b +} + +.inline-content { + flex-grow: 1; +} \ No newline at end of file diff --git a/examples/03-ui-components/13-equation/tsconfig.json b/examples/03-ui-components/13-equation/tsconfig.json new file mode 100644 index 0000000000..1bd8ab3c57 --- /dev/null +++ b/examples/03-ui-components/13-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/03-ui-components/13-equation/vite.config.ts b/examples/03-ui-components/13-equation/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/03-ui-components/13-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 14e278c208..49ebbd228a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17951,13 +17951,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" }, @@ -29383,6 +29384,7 @@ "@liveblocks/client": "^1.10.0", "@liveblocks/yjs": "^1.10.0", "@mantine/core": "^7.10.1", + "@types/katex": "^0.16.7", "@uppy/core": "^3.13.1", "@uppy/dashboard": "^3.9.1", "@uppy/drag-drop": "^3.1.1", @@ -29394,6 +29396,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", @@ -29402,6 +29405,7 @@ "yjs": "^13.6.15" }, "devDependencies": { + "@types/katex": "^0.16.7", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^4.0.4", diff --git a/playground/package.json b/playground/package.json index 0ee1d1f6c7..45b44a2aeb 100644 --- a/playground/package.json +++ b/playground/package.json @@ -21,6 +21,7 @@ "@liveblocks/client": "^1.10.0", "@liveblocks/yjs": "^1.10.0", "@mantine/core": "^7.10.1", + "@types/katex": "^0.16.7", "@uppy/core": "^3.13.1", "@uppy/dashboard": "^3.9.1", "@uppy/drag-drop": "^3.1.1", @@ -32,6 +33,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", @@ -40,6 +42,7 @@ "yjs": "^13.6.15" }, "devDependencies": { + "@types/katex": "^0.16.7", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^4.0.4", diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index eaac162755..deab6d212c 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -554,6 +554,33 @@ "slug": "ui-components" } }, + { + "projectSlug": "equation", + "fullSlug": "ui-components/equation", + "pathFromRoot": "examples/03-ui-components/13-equation", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "Custom Schemas", + "Suggestion Menus", + "Slash Menu" + ], + "dependencies": { + "katex": "^0.16.11", + "@types/katex": "^0.16.11", + "react-icons": "^5.2.1" + } as any + }, + "title": "Equation", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, { "projectSlug": "link-toolbar-buttons", "fullSlug": "ui-components/link-toolbar-buttons", From 536f3fb0b7a800ce1b539a36ec2b0aa566269487 Mon Sep 17 00:00:00 2001 From: jkcs <1778768609@qq.com> Date: Fri, 19 Jul 2024 14:36:08 +0800 Subject: [PATCH 2/5] Inline Equation --- examples/03-ui-components/13-equation/App.tsx | 49 ++-- .../03-ui-components/13-equation/Equation.tsx | 245 +++++++++++++----- .../03-ui-components/13-equation/styles.css | 106 ++++---- 3 files changed, 250 insertions(+), 150 deletions(-) diff --git a/examples/03-ui-components/13-equation/App.tsx b/examples/03-ui-components/13-equation/App.tsx index 567bb52e9a..f7e55ed27f 100644 --- a/examples/03-ui-components/13-equation/App.tsx +++ b/examples/03-ui-components/13-equation/App.tsx @@ -2,7 +2,6 @@ import { BlockNoteSchema, defaultInlineContentSpecs, filterSuggestionItems, - insertOrUpdateBlock, } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; import { @@ -11,39 +10,36 @@ import { useCreateBlockNote, } from "@blocknote/react"; import { BlockNoteView } from "@blocknote/mantine"; +import { PiTextSuperscript } from "react-icons/pi"; import "@blocknote/mantine/style.css"; -import { LaTex } from "./Equation"; +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, - latex: LaTex, + inlineEquation: InlineEquation, }, }); // Slash menu item to insert an Alert block const insertLaTex = (editor: typeof schema.BlockNoteEditor) => ({ - title: "latex", - key: "latex", - subtext: "Used for a top-level heading", - aliases: ["latex", "heading1", "h1"], + icon: PiTextSuperscript, + title: "Inline Equation", + key: "inlineEquation", + subtext: "Insert mathematical symbols in text.", + aliases: ["equation", "latex", "katex"], group: "Other", onItemClick: () => { - insertOrUpdateBlock(editor, { - type: "paragraph", - content: [ - { - type: "latex", - props: { - open: true, - }, - content: "\\sqrt{a^2 + b^2}", - }, - ], - }); + const view = editor._tiptapEditor.view; + const pos = editor._tiptapEditor.state.selection.from; + const tr = view.state.tr.insert( + pos, + view.state.schema.nodes.inlineEquation.create(""), + ); + view.dispatch(tr); }, }); @@ -55,25 +51,22 @@ export default function App() { { type: "paragraph", content: [ - "latex text editor ", + "This is an example inline equation", { - type: "latex", + type: "inlineEquation", 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", - content: [ - { - type: "latex", - content: - "\\int \\frac{1}{\\sqrt{1-x^{2}}}\\mathrm{d}x= \\arcsin x +C", - }, - ], }, { type: "paragraph", diff --git a/examples/03-ui-components/13-equation/Equation.tsx b/examples/03-ui-components/13-equation/Equation.tsx index 8bd958ded7..90339f2237 100644 --- a/examples/03-ui-components/13-equation/Equation.tsx +++ b/examples/03-ui-components/13-equation/Equation.tsx @@ -1,6 +1,6 @@ -import { useComponentsContext } from "@blocknote/react"; +import { useComponentsContext,useEditorContentOrSelectionChange } from "@blocknote/react"; import { NodeView } from "prosemirror-view"; -import { BlockNoteEditor, propsToAttributes } from "@blocknote/core"; +import { BlockNoteEditor } from "@blocknote/core"; import { NodeViewProps, NodeViewRenderer, @@ -12,58 +12,169 @@ import { createInternalInlineContentSpec, } from "@blocknote/core"; import { mergeAttributes } from "@tiptap/core"; -import { ChangeEvent, useEffect, useState, useRef } from "react"; +import {ChangeEvent, useEffect, useState, useRef, useCallback, forwardRef, MouseEvent as ReactMouseEvent} from "react"; import katex from "katex"; +import { AiOutlineEnter } from "react-icons/ai"; import "katex/dist/katex.min.css"; import "./styles.css"; -function LaTexView() { + +const TextareaView = forwardRef((props: any, ref: any) => { + const { autofocus, ...rest } = props; + useEffect(() => { + if (autofocus && ref.current) { + ref.current.setSelectionRange(0, ref.current.value.length); + ref.current.focus() + } + }, [autofocus, ref]); + + return ( + <textarea + ref={ref} + className={"equation-textarea"} + value={props.value} + onChange={props.onChange} + {...rest} + /> + ) +}); + +function InlineEquationView() { const nodeView: - | ((this: { - name: string; - options: any; - storage: any; - editor: any; - type: any; - parent: any; + | ((this: { + name: string; + options: any; + storage: any; + editor: any; + type: any; + parent: any; }) => NodeViewRenderer) | null = function () { const BlockContent = (props: NodeViewProps & { selectionHack: any }) => { /* eslint-disable react-hooks/rules-of-hooks */ const editor: BlockNoteEditor<any> = this.options.editor; const content = props.node.textContent; - const open = props.node.attrs.open; + const nodeSize = props.node.nodeSize; const textareaRef = useRef<HTMLTextAreaElement | null>(null); const contentRef = useRef<HTMLElement | null>(null); + const containerRef = useRef<HTMLElement | null>(null); const [html, setHtml] = useState(""); - const [loading, setLoading] = useState(false); + const [focus, setFocus] = useState(!content); + const [curEdge, setCurEdge] = useState(!content); const Components = useComponentsContext()!; + 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, + } + } + useEffect(() => { - setLoading(true); const html = katex.renderToString(content, { throwOnError: false, }); setHtml(html); - setLoading(false); }, [content]); + useEditorContentOrSelectionChange(() => { + const pos = props.getPos?.(); + const courPos = editor._tiptapEditor.state.selection.from; + const selection = editor.getSelection(); + + setCurEdge(!selection && (courPos === pos + nodeSize || courPos === pos)); + }) + + useEffect(() => { - if (open) { - if (contentRef.current) { - contentRef.current.click(); + if (focus) { + contentRef.current?.click(); + } + }, [focus]); + + const handleEnter = useCallback((event: ReactMouseEvent | KeyboardEvent) => { + const pos = props.getPos?.(); + event.preventDefault(); + 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, nodeSize, props]); + + const handleMenuNavigationKeys = useCallback((event: KeyboardEvent) => { + const textareaEdge = getTextareaEdge(); + const pos = props.getPos?.(); + const courPos = editor._tiptapEditor.state.selection.from; + + if (event.key === "ArrowLeft") { + if (courPos === pos + nodeSize && !focus) { + setFocus(true); } - setTimeout(() => { - if (textareaRef.current) { - textareaRef.current?.focus(); - textareaRef.current?.setSelectionRange( - textareaRef.current.value.length, - textareaRef.current.value.length - ); - } - }); + 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, handleEnter, nodeSize, props]); + + useEffect(() => { + if (focus || curEdge) { + editor.domElement.addEventListener( + "keydown", + handleMenuNavigationKeys, + true + ); } - }, [open]); + + return () => { + editor.domElement.removeEventListener( + "keydown", + handleMenuNavigationKeys, + true + ); + }; + }, [editor.domElement, focus, handleMenuNavigationKeys, curEdge]); const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => { const val = e.target.value; @@ -74,7 +185,7 @@ function LaTexView() { const tr = view.state.tr.replaceWith( pos, pos + node.nodeSize, - view.state.schema.nodes.latex.create( + view.state.schema.nodes.inlineEquation.create( { ...node.attrs, }, @@ -83,31 +194,51 @@ function LaTexView() { ); 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 ( - <NodeViewWrapper as={"span"}> - <Components.Generic.Popover.Root> + <NodeViewWrapper as={"span"} ref={containerRef}> + <Components.Generic.Popover.Root opened={focus}> <Components.Generic.Popover.Trigger> - <span className={"latex"} ref={contentRef}> - {loading ? ( - <span className={"latex-loading"}>latex loading...</span> + <span className={"equation " + (focus ? 'focus' : '')} ref={contentRef}> + {!content ? ( + <span onClick={() => setFocus(true)} className={"equation-empty"}> + New Equation + </span> ) : ( <span - className={"latex-content"} + onClick={() => setFocus(true)} className={"equation-content"} dangerouslySetInnerHTML={{ __html: html }}></span> )} </span> </Components.Generic.Popover.Trigger> <Components.Generic.Popover.Content - className={"bn-popover-content bn-form-popover"} - variant={"form-popover"}> - <textarea - ref={textareaRef} - className={"latex-textarea"} - value={content} - onChange={handleChange} - /> + className={"bn-popover-content bn-form-popover"} + variant={"form-popover"}> + <label className={"equation-label"}> + <TextareaView + placeholder={"c^2 = a^2 + b^2"} + ref={textareaRef} + autofocus + value={content} + onChange={handleChange} + /> + <span onClick={handleEnter} className={"equation-enter"}><AiOutlineEnter /></span> + </label> </Components.Generic.Popover.Content> </Components.Generic.Popover.Root> </NodeViewWrapper> @@ -152,38 +283,27 @@ function LaTexView() { return nodeView; } -const propSchema = { - open: { - type: "boolean", - default: false, - }, -}; - const node = createStronglyTypedTiptapNode({ - name: "latex", + name: "inlineEquation", inline: true, group: "inline", content: "inline*", editable: true, selectable: false, - - addAttributes() { - return propsToAttributes(propSchema); - }, - parseHTML() { return [ { - tag: "latex", + tag: "inlineEquation", priority: 200, - node: "latex", + node: "inlineEquation", }, ]; }, + // @ts-ignore renderHTML({ HTMLAttributes }) { return [ - "latex", + "inlineEquation", mergeAttributes(HTMLAttributes, { "data-content-type": this.name, }), @@ -191,14 +311,13 @@ const node = createStronglyTypedTiptapNode({ ]; }, - addNodeView: LaTexView(), + addNodeView: InlineEquationView(), }); -export const LaTex = createInternalInlineContentSpec( +export const InlineEquation = createInternalInlineContentSpec( { content: "styled", - type: "latex", - propSchema: propSchema, + type: "inlineEquation", }, { node, diff --git a/examples/03-ui-components/13-equation/styles.css b/examples/03-ui-components/13-equation/styles.css index 7296b28dce..b1793b3e9c 100644 --- a/examples/03-ui-components/13-equation/styles.css +++ b/examples/03-ui-components/13-equation/styles.css @@ -1,74 +1,62 @@ -.alert { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; +.equation-content { + caret-color: rgb(55, 53, 47); + padding: 2px 2px; border-radius: 4px; - min-height: 48px; - padding: 4px; + transform: translate3d(-4px, 0, 0); + margin-right: -4px; + white-space: pre; } -.alert[data-alert-type="warning"] { - background-color: #fff6e6; -} - -.alert[data-alert-type="error"] { - background-color: #ffe6e6; -} -.alert[data-alert-type="info"] { - background-color: #e6ebff; +.equation.focus .equation-content { + background: rgba(37, 135, 231, 0.12); } -.alert[data-alert-type="success"] { - background-color: #e6ffe6; -} - -[data-color-scheme="dark"] .alert[data-alert-type="warning"] { - background-color: #805d20; -} - -[data-color-scheme="dark"] .alert[data-alert-type="error"] { - background-color: #802020; +.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; } -[data-color-scheme="dark"] .alert[data-alert-type="info"] { - background-color: #203380; +.equation-label { + display: flex; + align-items: flex-start; + justify-content: flex-start; + padding: 4px; } -[data-color-scheme="dark"] .alert[data-alert-type="success"] { - background-color: #208020; +.equation-textarea { + min-width: 200px; + margin-right: 10px; + outline: none; + border: none; + font-size: 14px; } -.alert-icon-wrapper { - border-radius: 16px; - display: flex; - justify-content: center; - align-items: center; - margin-left: 12px; - margin-right: 12px; - height: 18px; - width: 18px; +.equation-enter { user-select: none; cursor: pointer; -} - -.alert-icon[data-alert-icon-type="warning"] { - color: #e69819 -} - -.alert-icon[data-alert-icon-type="error"] { - color: #d80d0d -} - -.alert-icon[data-alert-icon-type="info"] { - color: #507aff -} - -.alert-icon[data-alert-icon-type="success"] { - color: #0bc10b -} - -.inline-content { - flex-grow: 1; + 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 From aafcc4f5ec49c1e74ca21e62dfb614277032ca01 Mon Sep 17 00:00:00 2001 From: jkcs <1778768609@qq.com> Date: Fri, 19 Jul 2024 14:49:12 +0800 Subject: [PATCH 3/5] Inline Equation --- .../03-ui-components/13-equation/.bnexample.json | 2 +- examples/03-ui-components/13-equation/App.tsx | 6 +++--- examples/03-ui-components/13-equation/index.html | 2 +- playground/src/examples.gen.tsx | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/03-ui-components/13-equation/.bnexample.json b/examples/03-ui-components/13-equation/.bnexample.json index 1aadbc234b..1e4b6d5d3e 100644 --- a/examples/03-ui-components/13-equation/.bnexample.json +++ b/examples/03-ui-components/13-equation/.bnexample.json @@ -2,7 +2,7 @@ "playground": true, "docs": false, "author": "matthewlipski", - "tags": ["Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", "Slash Menu"], + "tags": ["Equation", "Inline Equation", "Custom Schemas", "Latex", "Slash Menu"], "dependencies": { "katex": "^0.16.11", "@types/katex": "^0.16.7", diff --git a/examples/03-ui-components/13-equation/App.tsx b/examples/03-ui-components/13-equation/App.tsx index f7e55ed27f..20df20adbf 100644 --- a/examples/03-ui-components/13-equation/App.tsx +++ b/examples/03-ui-components/13-equation/App.tsx @@ -10,7 +10,7 @@ import { useCreateBlockNote, } from "@blocknote/react"; import { BlockNoteView } from "@blocknote/mantine"; -import { PiTextSuperscript } from "react-icons/pi"; +import { RiFormula } from "react-icons/ri"; import "@blocknote/mantine/style.css"; import { InlineEquation } from "./Equation"; @@ -26,7 +26,7 @@ const schema = BlockNoteSchema.create({ // Slash menu item to insert an Alert block const insertLaTex = (editor: typeof schema.BlockNoteEditor) => ({ - icon: PiTextSuperscript, + icon: <RiFormula size={18}/>, title: "Inline Equation", key: "inlineEquation", subtext: "Insert mathematical symbols in text.", @@ -51,7 +51,7 @@ export default function App() { { type: "paragraph", content: [ - "This is an example inline equation", + "This is an example inline equation ", { type: "inlineEquation", content: "c = \\pm\\sqrt{a^2 + b^2}", diff --git a/examples/03-ui-components/13-equation/index.html b/examples/03-ui-components/13-equation/index.html index 6c6a363839..84532134d8 100644 --- a/examples/03-ui-components/13-equation/index.html +++ b/examples/03-ui-components/13-equation/index.html @@ -5,7 +5,7 @@ </script> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Equation</title> + <title>Alert Block</title> </head> <body> <div id="root"></div> diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index deab6d212c..b7364ed1a9 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -560,22 +560,22 @@ "pathFromRoot": "examples/03-ui-components/13-equation", "config": { "playground": true, - "docs": true, + "docs": false, "author": "matthewlipski", "tags": [ - "Intermediate", - "Blocks", + "Equation", + "Inline Equation", "Custom Schemas", - "Suggestion Menus", + "Latex", "Slash Menu" ], "dependencies": { "katex": "^0.16.11", - "@types/katex": "^0.16.11", + "@types/katex": "^0.16.7", "react-icons": "^5.2.1" } as any }, - "title": "Equation", + "title": "Alert Block", "group": { "pathFromRoot": "examples/03-ui-components", "slug": "ui-components" From ca63b7ea2361200cac4aa65bd99e8809d69b88c9 Mon Sep 17 00:00:00 2001 From: jkcs <1778768609@qq.com> Date: Fri, 19 Jul 2024 15:00:12 +0800 Subject: [PATCH 4/5] fix: type error --- examples/03-ui-components/13-equation/App.tsx | 2 +- examples/03-ui-components/13-equation/Equation.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/03-ui-components/13-equation/App.tsx b/examples/03-ui-components/13-equation/App.tsx index 20df20adbf..42c3fcc292 100644 --- a/examples/03-ui-components/13-equation/App.tsx +++ b/examples/03-ui-components/13-equation/App.tsx @@ -37,7 +37,7 @@ const insertLaTex = (editor: typeof schema.BlockNoteEditor) => ({ const pos = editor._tiptapEditor.state.selection.from; const tr = view.state.tr.insert( pos, - view.state.schema.nodes.inlineEquation.create(""), + view.state.schema.nodes.inlineEquation.create(), ); view.dispatch(tr); }, diff --git a/examples/03-ui-components/13-equation/Equation.tsx b/examples/03-ui-components/13-equation/Equation.tsx index 90339f2237..7eace7beb3 100644 --- a/examples/03-ui-components/13-equation/Equation.tsx +++ b/examples/03-ui-components/13-equation/Equation.tsx @@ -318,6 +318,7 @@ export const InlineEquation = createInternalInlineContentSpec( { content: "styled", type: "inlineEquation", + propSchema: {}, }, { node, From 41948a5ca794afb48b750800e84935e1f4ac37f4 Mon Sep 17 00:00:00 2001 From: jkcs <1778768609@qq.com> Date: Fri, 19 Jul 2024 15:17:18 +0800 Subject: [PATCH 5/5] fix: title error --- examples/03-ui-components/13-equation/README.md | 6 +++--- examples/03-ui-components/13-equation/index.html | 2 +- playground/src/examples.gen.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/03-ui-components/13-equation/README.md b/examples/03-ui-components/13-equation/README.md index 8c064c8b11..2e140b5666 100644 --- a/examples/03-ui-components/13-equation/README.md +++ b/examples/03-ui-components/13-equation/README.md @@ -1,8 +1,8 @@ -# Alert Block +# Inline Equation -In this example, we create a custom `Alert` block which is used to emphasize text. In addition, we create a Slash Menu item which inserts an `Alert` block. +In this example, we create a custom `Inline Equation` -**Try it out:** Press the "/" key to open the Slash Menu and insert an `Alert` block! +**Try it out:** Press the "/" key to open the Slash Menu and insert an `Equation` block! **Relevant Docs:** diff --git a/examples/03-ui-components/13-equation/index.html b/examples/03-ui-components/13-equation/index.html index 84532134d8..772555bb29 100644 --- a/examples/03-ui-components/13-equation/index.html +++ b/examples/03-ui-components/13-equation/index.html @@ -5,7 +5,7 @@ </script> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Alert Block</title> + <title>Inline Equation</title> </head> <body> <div id="root"></div> diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index b7364ed1a9..e6e3d9961d 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -575,7 +575,7 @@ "react-icons": "^5.2.1" } as any }, - "title": "Alert Block", + "title": "Inline Equation", "group": { "pathFromRoot": "examples/03-ui-components", "slug": "ui-components"