-
-
Notifications
You must be signed in to change notification settings - Fork 759
docs: Updated vanilla docs and added example (BLO-1010) #2915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| "custom-schema", | ||
| "collaboration", | ||
| "extensions", | ||
| "ai" | ||
| "ai", | ||
| "vanilla-js" | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "playground": true, | ||
| "docs": true, | ||
| "author": "matthewlipski", | ||
| "tags": ["Advanced", "UI Components", "Block Side Menu"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Custom Side Menu (Vanilla JS) | ||
|
|
||
| This example uses the vanilla JS API to create a plain BlockNote editor without `@blocknote/react`, mounting it manually and building a custom Side Menu from scratch. | ||
|
|
||
| **Try it out:** Hover over a block to reveal the custom Side Menu, then use the `+` button to add a block or the `::` handle to drag it! | ||
|
|
||
| **Relevant Docs:** | ||
|
|
||
| - [Getting Started with Vanilla JS](/docs/getting-started/vanilla-js) | ||
| - [Editor Setup](/docs/getting-started/editor-setup) | ||
| - [Extensions](/docs/features/extensions) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Custom Side Menu (Vanilla JS)</title> | ||
| <script> | ||
| <!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY --> | ||
| </script> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="./main.tsx"></script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY | ||
| import React from "react"; | ||
| import { createRoot } from "react-dom/client"; | ||
| import App from "./src/App.jsx"; | ||
|
|
||
| const root = createRoot(document.getElementById("root")!); | ||
| root.render( | ||
| <React.StrictMode> | ||
| <App /> | ||
| </React.StrictMode>, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "name": "@blocknote/example-vanilla-js-vanilla-custom-side-menu", | ||
| "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", | ||
| "type": "module", | ||
| "private": true, | ||
| "version": "0.12.4", | ||
| "scripts": { | ||
| "start": "vp dev", | ||
| "dev": "vp dev", | ||
| "build:prod": "tsc && vp build", | ||
| "preview": "vp preview" | ||
| }, | ||
| "dependencies": { | ||
| "@blocknote/ariakit": "latest", | ||
| "@blocknote/core": "latest", | ||
| "@blocknote/mantine": "latest", | ||
| "@blocknote/react": "latest", | ||
| "@blocknote/shadcn": "latest", | ||
| "@mantine/core": "^9.0.2", | ||
| "@mantine/hooks": "^9.0.2", | ||
| "react": "^19.2.3", | ||
| "react-dom": "^19.2.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/react": "^19.2.3", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "^6.0.1", | ||
| "vite-plus": "catalog:" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import { | ||
| BlockNoteEditor, | ||
| SideMenuExtension, | ||
| SuggestionMenu, | ||
| } from "@blocknote/core"; | ||
| import "@blocknote/core/fonts/inter.css"; | ||
| import "@blocknote/core/style.css"; | ||
| import { useEffect, useRef } from "react"; | ||
|
|
||
| import "./styles.css"; | ||
|
|
||
| // Creates a simple button element, used for the side menu below. | ||
| function createButton(text: string, onClick?: () => void) { | ||
| const element = document.createElement("a"); | ||
| element.className = "side-menu-button"; | ||
| element.href = "#"; | ||
| element.text = text; | ||
|
|
||
| if (onClick) { | ||
| element.addEventListener("click", (e) => { | ||
| onClick(); | ||
| e.preventDefault(); | ||
| }); | ||
| } | ||
|
|
||
| return element; | ||
| } | ||
|
|
||
| // Since this example doesn't use `@blocknote/react`, we can't use the built-in | ||
| // UI components. Instead, we create a plain BlockNote editor using the vanilla | ||
| // JS API and mount it manually, then wire up our own Side Menu. | ||
| function initEditor(rootElement: HTMLElement) { | ||
| const editor = BlockNoteEditor.create(); | ||
|
|
||
| // `editor.mount` turns the element it's given into the editable area, so we | ||
| // mount into a dedicated child element. This keeps our custom UI elements | ||
| // (like the Side Menu below) separate from the editor's content. | ||
| const editorElement = document.createElement("div"); | ||
| rootElement.appendChild(editorElement); | ||
| editor.mount(editorElement); | ||
|
|
||
| const sideMenu = editor.getExtension(SideMenuExtension)!; | ||
|
Comment on lines
+32
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Cleanup doesn't remove the appended DOM nodes, leaking elements on remount/StrictMode double-invoke.
🐛 Proposed fix return () => {
unsubscribe();
editor.unmount();
+ editorElement.remove();
+ element?.remove();
};Also applies to: 108-128, 130-139 🤖 Prompt for AI Agents |
||
|
|
||
| let element: HTMLElement; | ||
|
|
||
| // The Side Menu extension exposes a store which holds its state (visibility, | ||
| // position, and the block it's attached to). We subscribe to it to update | ||
| // our custom element whenever the state changes. | ||
| const unsubscribe = sideMenu.store.subscribe(() => { | ||
| const sideMenuState = sideMenu.store.state; | ||
|
|
||
| // The store's initial state is `undefined`, so we wait until the side menu | ||
| // has a block to attach to. | ||
| if (!sideMenuState) { | ||
| return; | ||
| } | ||
|
|
||
| // We create the element the first time the side menu is shown, and reuse it | ||
| // afterwards. Since the element is only created once, its event handlers | ||
| // read the block to act on from the store, so they always use the block the | ||
| // side menu is currently attached to. | ||
| if (!element) { | ||
| element = document.createElement("div"); | ||
| element.className = "side-menu"; | ||
| element.style.position = "fixed"; | ||
| const addBtn = createButton("+", () => { | ||
| const block = sideMenu.store.state?.block; | ||
| if (!block) { | ||
| return; | ||
| } | ||
|
|
||
| const blockContent = block.content; | ||
| const isBlockEmpty = | ||
| blockContent !== undefined && | ||
| Array.isArray(blockContent) && | ||
| blockContent.length === 0; | ||
|
|
||
| if (isBlockEmpty) { | ||
| editor.setTextCursorPosition(block); | ||
| editor.getExtension(SuggestionMenu)?.openSuggestionMenu("/"); | ||
| } else { | ||
| const insertedBlock = editor.insertBlocks( | ||
| [{ type: "paragraph" }], | ||
| block, | ||
| "after", | ||
| )[0]; | ||
| editor.setTextCursorPosition(insertedBlock); | ||
| editor.getExtension(SuggestionMenu)?.openSuggestionMenu("/"); | ||
| } | ||
| }); | ||
| element.appendChild(addBtn); | ||
|
|
||
| const dragBtn = createButton("::", () => { | ||
| // Intentionally empty - dragging is handled by the drag events below. | ||
| }); | ||
|
|
||
| dragBtn.addEventListener("dragstart", (evt) => { | ||
| const block = sideMenu.store.state?.block; | ||
| if (block) { | ||
| sideMenu.blockDragStart(evt, block); | ||
| } | ||
| }); | ||
| dragBtn.addEventListener("dragend", () => sideMenu.blockDragEnd()); | ||
| dragBtn.draggable = true; | ||
| element.style.display = "none"; | ||
| element.appendChild(dragBtn); | ||
|
|
||
| // The Side Menu is appended to the root element rather than the editor | ||
| // element, so that the editor doesn't treat it as content. | ||
| rootElement.appendChild(element); | ||
| } | ||
|
|
||
| if (sideMenuState.show) { | ||
| element.style.display = "flex"; | ||
|
|
||
| element.style.top = sideMenuState.referencePos.top + "px"; | ||
| element.style.left = | ||
| sideMenuState.referencePos.x - element.offsetWidth + "px"; | ||
| } else { | ||
| element.style.display = "none"; | ||
| } | ||
| }); | ||
|
|
||
| return () => { | ||
| unsubscribe(); | ||
| editor.unmount(); | ||
| }; | ||
| } | ||
|
|
||
| export default function App() { | ||
| const rootRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (!rootRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| return initEditor(rootRef.current); | ||
| }, []); | ||
|
|
||
| return <div ref={rootRef} />; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add
<!DOCTYPE html>.Static analysis (HTMLHint) flags the missing doctype declaration, which causes the page to render in quirks mode.
🐛 Proposed fix
+<!DOCTYPE html> <html lang="en">📝 Committable suggestion
🧰 Tools
🪛 HTMLHint (1.9.2)
[error] 1-1: Doctype must be declared before any non-comment content.
(doctype-first)
🤖 Prompt for AI Agents
Source: Linters/SAST tools