diff --git a/.gitignore b/.gitignore
index 61030b7716..ef4e11d35d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,8 @@
# dependencies
node_modules
dist
-types
+packages/*/types
+examples/*/types
/.pnp
.pnp.js
diff --git a/examples/editor/package.json b/examples/editor/package.json
index 9ad54b2fe1..b8145f8c85 100644
--- a/examples/editor/package.json
+++ b/examples/editor/package.json
@@ -10,12 +10,13 @@
},
"dependencies": {
"@blocknote/core": "^0.1.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2"
+ "@blocknote/react": "^0.1.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
},
"devDependencies": {
- "@types/react": "^17.0.39",
- "@types/react-dom": "^17.0.12",
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^1.0.7",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx
index 20a635af92..1c6c5c7f4e 100644
--- a/examples/editor/src/App.tsx
+++ b/examples/editor/src/App.tsx
@@ -1,6 +1,6 @@
// import logo from './logo.svg'
-import { EditorContent, useEditor } from "@blocknote/core";
import "@blocknote/core/style.css";
+import { BlockNoteView, useBlockNote } from "@blocknote/react";
import { Editor } from "@tiptap/core";
import styles from "./App.module.css";
@@ -8,7 +8,7 @@ type WindowWithProseMirror = Window &
typeof globalThis & { ProseMirror: Editor };
function App() {
- const editor = useEditor({
+ const editor = useBlockNote({
onUpdate: ({ editor }) => {
console.log(editor.getJSON());
(window as WindowWithProseMirror).ProseMirror = editor; // Give tests a way to get editor instance
@@ -21,7 +21,7 @@ function App() {
},
});
- return ;
+ return ;
}
export default App;
diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx
index 3a568f9845..0808f1a4d8 100644
--- a/examples/editor/src/main.tsx
+++ b/examples/editor/src/main.tsx
@@ -1,14 +1,13 @@
import React from "react";
-import ReactDOM from "react-dom";
+import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
window.React = React;
-window.ReactDOM = ReactDOM;
-ReactDOM.render(
+const root = createRoot(document.getElementById("root")!);
+root.render(
- ,
- document.getElementById("root")
+
);
diff --git a/examples/editor/vite.config.ts b/examples/editor/vite.config.ts
index 9c31e3ff42..d135de28dc 100644
--- a/examples/editor/vite.config.ts
+++ b/examples/editor/vite.config.ts
@@ -8,17 +8,24 @@ export default defineConfig((conf) => ({
optimizeDeps: {
// link: ['vite-react-ts-components'],
},
+ build: {
+ sourcemap: true,
+ },
resolve: {
alias:
conf.command === "build"
? {}
: {
- // Comment out the line below to load a built version of blocknote
+ // 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/"
+ ),
},
},
}));
diff --git a/examples/vanilla/.gitignore b/examples/vanilla/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/examples/vanilla/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/vanilla/README.md b/examples/vanilla/README.md
new file mode 100644
index 0000000000..868cba437c
--- /dev/null
+++ b/examples/vanilla/README.md
@@ -0,0 +1,3 @@
+# Vanilla editor example
+
+This is an example client application that consumes @blocknote/core.
diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html
new file mode 100644
index 0000000000..dc902f4b5b
--- /dev/null
+++ b/examples/vanilla/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ BlockNote demo vanilla js
+
+
+
+
+
+
diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json
new file mode 100644
index 0000000000..9f5a7e0cdf
--- /dev/null
+++ b/examples/vanilla/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-vanilla",
+ "private": true,
+ "version": "0.1.2",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint src --max-warnings 0"
+ },
+ "dependencies": {
+ "@blocknote/core": "^0.1.2"
+ },
+ "devDependencies": {
+ "eslint": "^8.10.0",
+ "eslint-config-react-app": "^7.0.0",
+ "typescript": "^4.5.4",
+ "vite": "^3.0.5",
+ "vite-plugin-eslint": "^1.7.0"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ],
+ "rules": {
+ "curly": 1
+ }
+ }
+}
diff --git a/examples/vanilla/src/index.css b/examples/vanilla/src/index.css
new file mode 100644
index 0000000000..d8ab4ad4a3
--- /dev/null
+++ b/examples/vanilla/src/index.css
@@ -0,0 +1,10 @@
+html,
+body,
+#root {
+ height: 100%;
+}
+
+.editor {
+ padding: 0 calc((100% - 731px) / 2);
+ height: 100%;
+}
diff --git a/examples/vanilla/src/main.tsx b/examples/vanilla/src/main.tsx
new file mode 100644
index 0000000000..2929893849
--- /dev/null
+++ b/examples/vanilla/src/main.tsx
@@ -0,0 +1,31 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import "./index.css";
+import { blockSideMenuFactory } from "./ui/blockSideMenuFactory";
+import { formattingToolbarFactory } from "./ui/formattingToolbarFactory";
+import { hyperlinkToolbarFactory } from "./ui/hyperlinkToolbarFactory";
+import { slashMenuFactory } from "./ui/slashMenuFactory";
+
+const editor = new BlockNoteEditor({
+ element: document.getElementById("root")!,
+ uiFactories: {
+ // Create an example formatting toolbar which just consists of a bold toggle
+ formattingToolbarFactory,
+ // Create an example menu for hyperlinks
+ hyperlinkToolbarFactory,
+ // Create an example menu for the /-menu
+ slashMenuFactory: slashMenuFactory,
+ // Create an example menu for when a block is hovered
+ blockSideMenuFactory,
+ },
+ onUpdate: ({ editor }) => {
+ console.log(editor.getJSON());
+ (window as any).ProseMirror = editor; // Give tests a way to get editor instance
+ },
+ editorProps: {
+ attributes: {
+ class: "editor",
+ },
+ },
+});
+
+console.log("editor created", editor);
diff --git a/examples/vanilla/src/ui/blockSideMenuFactory.ts b/examples/vanilla/src/ui/blockSideMenuFactory.ts
new file mode 100644
index 0000000000..5c71b94f46
--- /dev/null
+++ b/examples/vanilla/src/ui/blockSideMenuFactory.ts
@@ -0,0 +1,46 @@
+import { BlockSideMenuFactory } from "@blocknote/core";
+import { createButton } from "./util";
+
+/**
+ * This menu is drawn next to a block, when it's hovered over
+ * It renders a drag handle and + button to create a new block
+ */
+export const blockSideMenuFactory: BlockSideMenuFactory = (staticParams) => {
+ const container = document.createElement("div");
+ container.style.background = "gray";
+ container.style.position = "absolute";
+ container.style.padding = "10px";
+ container.style.opacity = "0.8";
+ const addBtn = createButton("+", () => {
+ staticParams.addBlock();
+ });
+ container.appendChild(addBtn);
+
+ const dragBtn = createButton("::", () => {
+ // TODO: render a submenu with a delete option that calls "props.deleteBlock"
+ });
+
+ dragBtn.addEventListener("dragstart", staticParams.blockDragStart);
+ dragBtn.addEventListener("dragend", staticParams.blockDragEnd);
+ container.style.display = "none";
+ container.appendChild(dragBtn);
+
+ document.body.appendChild(container);
+
+ return {
+ element: container,
+ render: (params, isHidden) => {
+ if (isHidden) {
+ container.style.display = "block";
+ }
+
+ console.log("show blockmenu", params);
+ container.style.top = params.blockBoundingBox.y + "px";
+ container.style.left =
+ params.blockBoundingBox.x - container.offsetWidth + "px";
+ },
+ hide: () => {
+ container.style.display = "none";
+ },
+ };
+};
diff --git a/examples/vanilla/src/ui/formattingToolbarFactory.ts b/examples/vanilla/src/ui/formattingToolbarFactory.ts
new file mode 100644
index 0000000000..b78e4fa94d
--- /dev/null
+++ b/examples/vanilla/src/ui/formattingToolbarFactory.ts
@@ -0,0 +1,45 @@
+import { FormattingToolbarFactory } from "@blocknote/core";
+import { createButton } from "./util";
+
+/**
+ * This menu is drawn when a piece of text is selected. We can use it to change formatting options
+ * such as bold, italic, indentation, etc.
+ */
+export const formattingToolbarFactory: FormattingToolbarFactory = (
+ staticParams
+) => {
+ const container = document.createElement("div");
+ container.style.background = "gray";
+ container.style.position = "absolute";
+ container.style.padding = "10px";
+ container.style.opacity = "0.8";
+ const boldBtn = createButton("set bold", () => {
+ staticParams.toggleBold();
+ });
+ container.appendChild(boldBtn);
+
+ const linkBtn = createButton("set link", () => {
+ staticParams.setHyperlink("https://www.google.com");
+ });
+
+ container.appendChild(boldBtn);
+ container.appendChild(linkBtn);
+ container.style.display = "none";
+ document.body.appendChild(container);
+
+ return {
+ element: container,
+ render: (params, isHidden) => {
+ if (isHidden) {
+ container.style.display = "block";
+ }
+
+ boldBtn.text = params.boldIsActive ? "unset bold" : "set bold";
+ container.style.top = params.selectionBoundingBox.y + "px";
+ container.style.left = params.selectionBoundingBox.x + "px";
+ },
+ hide: () => {
+ container.style.display = "none";
+ },
+ };
+};
diff --git a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts
new file mode 100644
index 0000000000..bfa7d67984
--- /dev/null
+++ b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts
@@ -0,0 +1,54 @@
+import { HyperlinkToolbarFactory } from "@blocknote/core";
+import { createButton } from "./util";
+
+/**
+ * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard),
+ * or when the mouse is hovering over a hyperlink
+ */
+export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (
+ staticParams
+) => {
+ const container = document.createElement("div");
+ container.style.background = "gray";
+ container.style.position = "absolute";
+ container.style.padding = "10px";
+ container.style.opacity = "0.8";
+
+ let url = "";
+ let text = "";
+
+ const editBtn = createButton("edit", () => {
+ const newUrl = prompt("new url") || url;
+ staticParams.editHyperlink(newUrl, text);
+ });
+
+ container.appendChild(editBtn);
+
+ const removeBtn = createButton("remove", () => {
+ staticParams.deleteHyperlink();
+ });
+
+ container.appendChild(editBtn);
+ container.appendChild(removeBtn);
+ container.style.display = "none";
+ document.body.appendChild(container);
+
+ return {
+ element: container,
+ render: (params, isHidden) => {
+ if (isHidden) {
+ url = params.url;
+ text = params.text;
+
+ container.style.display = "block";
+ }
+
+ console.log("show", params);
+ container.style.top = params.boundingBox.y + "px";
+ container.style.left = params.boundingBox.x + "px";
+ },
+ hide: () => {
+ container.style.display = "none";
+ },
+ };
+};
diff --git a/examples/vanilla/src/ui/slashMenuFactory.ts b/examples/vanilla/src/ui/slashMenuFactory.ts
new file mode 100644
index 0000000000..6dca65bea4
--- /dev/null
+++ b/examples/vanilla/src/ui/slashMenuFactory.ts
@@ -0,0 +1,60 @@
+import { SlashMenuItem, SuggestionsMenuFactory } from "@blocknote/core";
+import { createButton } from "./util";
+
+/**
+ * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard),
+ * or when the mouse is hovering over a hyperlink
+ */
+export const slashMenuFactory: SuggestionsMenuFactory = (
+ staticParams
+) => {
+ const container = document.createElement("div");
+ container.style.background = "gray";
+ container.style.position = "absolute";
+ container.style.padding = "10px";
+ container.style.opacity = "0.8";
+ container.style.display = "none";
+ document.body.appendChild(container);
+
+ function updateItems(
+ items: SlashMenuItem[],
+ onClick: (item: SlashMenuItem) => void,
+ selected: number
+ ) {
+ container.innerHTML = "";
+ const domItems = items.map((val, i) => {
+ const element = createButton(val.name, () => {
+ onClick(val);
+ });
+ element.style.display = "block";
+ if (selected === i) {
+ element.style.fontWeight = "bold";
+ }
+ return element;
+ });
+ container.append(...domItems);
+ return domItems;
+ }
+
+ return {
+ element: container,
+ render: (params, isHidden) => {
+ updateItems(
+ params.items,
+ staticParams.itemCallback,
+ params.selectedItemIndex
+ );
+
+ if (isHidden) {
+ container.style.display = "block";
+ }
+
+ console.log("show", params);
+ container.style.top = params.queryStartBoundingBox.y + "px";
+ container.style.left = params.queryStartBoundingBox.x + "px";
+ },
+ hide: () => {
+ container.style.display = "none";
+ },
+ };
+};
diff --git a/examples/vanilla/src/ui/util.ts b/examples/vanilla/src/ui/util.ts
new file mode 100644
index 0000000000..74e4c55eb1
--- /dev/null
+++ b/examples/vanilla/src/ui/util.ts
@@ -0,0 +1,11 @@
+export function createButton(text: string, onClick: () => void) {
+ const element = document.createElement("a");
+ element.href = "#";
+ element.text = text;
+ element.style.margin = "10px";
+ element.addEventListener("click", (e) => {
+ onClick();
+ e.preventDefault();
+ });
+ return element;
+}
diff --git a/examples/vanilla/src/vite-env.d.ts b/examples/vanilla/src/vite-env.d.ts
new file mode 100644
index 0000000000..11f02fe2a0
--- /dev/null
+++ b/examples/vanilla/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/vanilla/tsconfig.json b/examples/vanilla/tsconfig.json
new file mode 100644
index 0000000000..9387cb5ab6
--- /dev/null
+++ b/examples/vanilla/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "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"
+ // "paths": {
+ // "@blocknote/core": ["../../packages/core/src"]
+ // }
+ },
+ "include": ["src"],
+ "references": [
+ { "path": "./tsconfig.node.json" }
+ // { "path": "../../packages/core/tsconfig.json" }
+ ]
+}
diff --git a/examples/vanilla/tsconfig.node.json b/examples/vanilla/tsconfig.node.json
new file mode 100644
index 0000000000..e993792cb1
--- /dev/null
+++ b/examples/vanilla/tsconfig.node.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "esnext",
+ "moduleResolution": "node"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts
new file mode 100644
index 0000000000..fc8bd4a83d
--- /dev/null
+++ b/examples/vanilla/vite.config.ts
@@ -0,0 +1,26 @@
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [],
+ optimizeDeps: {
+ // link: ['vite-react-ts-components'],
+ },
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build"
+ ? {}
+ : {
+ // 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/"
+ ),
+ },
+ },
+}));
diff --git a/package-lock.json b/package-lock.json
index 1b635d9666..2af04677d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,12 +21,13 @@
"version": "0.1.2",
"dependencies": {
"@blocknote/core": "^0.1.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2"
+ "@blocknote/react": "^0.1.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
},
"devDependencies": {
- "@types/react": "^17.0.39",
- "@types/react-dom": "^17.0.12",
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^1.0.7",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
@@ -180,6 +181,7 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
"integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+ "dev": true,
"dependencies": {
"@babel/types": "^7.18.6"
},
@@ -2060,6 +2062,10 @@
"resolved": "examples/editor",
"link": true
},
+ "node_modules/@blocknote/react": {
+ "resolved": "packages/react",
+ "link": true
+ },
"node_modules/@emotion/babel-plugin": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz",
@@ -2109,14 +2115,6 @@
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
"integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
},
- "node_modules/@emotion/is-prop-valid": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
- "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
- "dependencies": {
- "@emotion/memoize": "^0.8.0"
- }
- },
"node_modules/@emotion/memoize": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
@@ -2167,11 +2165,6 @@
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
"integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
},
- "node_modules/@emotion/stylis": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
- "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
- },
"node_modules/@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
@@ -2252,24 +2245,24 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz",
- "integrity": "sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA=="
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.2.tgz",
+ "integrity": "sha512-Skfy0YS3NJ5nV9us0uuPN0HDk1Q4edljaOhRBJGDWs9EBa7ZVMYBHRFlhLvvmwEoaIM9BlH6QJFn9/uZg0bACg=="
},
"node_modules/@floating-ui/dom": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.3.tgz",
- "integrity": "sha512-6H1kwjkOZKabApNtXRiYHvMmYJToJ1DV7rQ3xc/WJpOABhQIOJJOdz2AOejj8X+gcybaFmBpisVTZxBZAM3V0w==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.6.tgz",
+ "integrity": "sha512-kt/tg1oip9OAH1xjCTcx1OpcUpu9rjDw3GKJ/rEhUqhO7QyJWfrHU0DpLTNsH67+JyFL5Kv9X1utsXwKFVtyEQ==",
"dependencies": {
- "@floating-ui/core": "^1.0.1"
+ "@floating-ui/core": "^1.0.2"
}
},
"node_modules/@floating-ui/react-dom": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.0.tgz",
- "integrity": "sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.1.tgz",
+ "integrity": "sha512-UW0t1Gi8ikbDRr8cQPVcqIDMBwUEENe5V4wlHWdrJ5egFnRQFBV9JirauTBFI6S8sM1qFUC1i+qa3g87E6CLTw==",
"dependencies": {
- "@floating-ui/dom": "^1.0.0"
+ "@floating-ui/dom": "^1.0.5"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -2277,9 +2270,9 @@
}
},
"node_modules/@floating-ui/react-dom-interactions": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.2.tgz",
- "integrity": "sha512-KhF+UN+MVqUx1bG1fe0aAiBl1hbz07Uin6UW70mxwUDhaGpitM16CYvGri1EqGY4hnWK8TQknDSP8iQFOxjhsg==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.3.tgz",
+ "integrity": "sha512-UEHqdnzyoiWNU5az/tAljr9iXFzN18DcvpMqW+/cXz4FEhDEB1ogLtWldOWCujLerPBnSRocADALafelOReMpw==",
"dependencies": {
"@floating-ui/react-dom": "^1.0.0",
"aria-hidden": "^1.1.3"
@@ -4224,10 +4217,12 @@
}
},
"node_modules/@tiptap/extension-bubble-menu": {
- "version": "2.0.0-beta.199",
- "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.199.tgz",
- "integrity": "sha512-T3K8xoDbX6J62lhIUpclQoW/1XFt7yfI5DCoxtVWUeKaF+pG6kdsB3CPG5C/+AQVlz2jSIJmQuPf8RQFpQs+yg==",
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.209.tgz",
+ "integrity": "sha512-tceZAuDpy3J96uGyCzpJFD3fHABJDTJTq5E0hm+TRQT+eVGVqZI0PE3/4yVFgkCshioTuJq8veMDFcqNsSkKsQ==",
"dependencies": {
+ "@tiptap/core": "^2.0.0-beta.209",
+ "lodash": "^4.17.21",
"prosemirror-state": "^1.4.1",
"prosemirror-view": "^1.28.2",
"tippy.js": "^6.3.7"
@@ -4237,7 +4232,37 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "^2.0.0-beta.193"
+ "@tiptap/core": "^2.0.0-beta.193",
+ "prosemirror-state": "^1.4.1",
+ "prosemirror-view": "^1.28.2"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu/node_modules/@tiptap/core": {
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.0-beta.209.tgz",
+ "integrity": "sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "prosemirror-commands": "^1.3.1",
+ "prosemirror-keymap": "^1.2.0",
+ "prosemirror-model": "^1.18.1",
+ "prosemirror-schema-list": "^1.2.2",
+ "prosemirror-state": "^1.4.1",
+ "prosemirror-transform": "^1.7.0",
+ "prosemirror-view": "^1.28.2"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu/node_modules/prosemirror-view": {
+ "version": "1.29.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz",
+ "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==",
+ "dependencies": {
+ "prosemirror-model": "^1.16.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
}
},
"node_modules/@tiptap/extension-code": {
@@ -4298,23 +4323,6 @@
"@tiptap/core": "^2.0.0-beta.193"
}
},
- "node_modules/@tiptap/extension-floating-menu": {
- "version": "2.0.0-beta.199",
- "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.199.tgz",
- "integrity": "sha512-ELjqnNbxW66uqg54zlP2b4EVYUWvT2WvHmeOXALzoLlNzbqUopIl3XNRsvU2Dv1W88C1UjKgnRZIkHKFE1X3CA==",
- "dependencies": {
- "prosemirror-state": "^1.4.1",
- "prosemirror-view": "^1.28.2",
- "tippy.js": "^6.3.7"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/core": "^2.0.0-beta.193"
- }
- },
"node_modules/@tiptap/extension-gapcursor": {
"version": "2.0.0-beta.199",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.199.tgz",
@@ -4450,12 +4458,12 @@
}
},
"node_modules/@tiptap/react": {
- "version": "2.0.0-beta.199",
- "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.199.tgz",
- "integrity": "sha512-AjBtoavcJ7WOoEXdJlrVEdEv6xuI5UFnqB88w8NlORSkWbfQ3uuOm3A0LUZ92/SsBz6NISZbsFahMy0DYgGbIA==",
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.209.tgz",
+ "integrity": "sha512-iuJ+hgaSPETPMX39QpX6e0tZmFAj9azl0qGhNm6NNB1biCehkB4qMfcfwecWFRWVpZKG5UtjJvjJ3UZM167Jlg==",
"dependencies": {
- "@tiptap/extension-bubble-menu": "^2.0.0-beta.199",
- "@tiptap/extension-floating-menu": "^2.0.0-beta.199",
+ "@tiptap/extension-bubble-menu": "^2.0.0-beta.209",
+ "@tiptap/extension-floating-menu": "^2.0.0-beta.209",
"prosemirror-view": "^1.28.2"
},
"funding": {
@@ -4468,6 +4476,33 @@
"react-dom": "^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@tiptap/react/node_modules/@tiptap/extension-floating-menu": {
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.209.tgz",
+ "integrity": "sha512-m5ucAguqDxuOvNcsmvuSLcN8TMkbhFmiC6dTJOyaAGjGn6d8Ly6aZh+lEwU228TebM0TKHTp8Xob1cLjV4TGgg==",
+ "dependencies": {
+ "tippy.js": "^6.3.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.193",
+ "prosemirror-state": "^1.4.1",
+ "prosemirror-view": "^1.28.2"
+ }
+ },
+ "node_modules/@tiptap/react/node_modules/prosemirror-view": {
+ "version": "1.29.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz",
+ "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==",
+ "dependencies": {
+ "prosemirror-model": "^1.16.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -4493,16 +4528,6 @@
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
"dev": true
},
- "node_modules/@types/hoist-non-react-statics": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
- "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
- "dev": true,
- "dependencies": {
- "@types/react": "*",
- "hoist-non-react-statics": "^3.3.0"
- }
- },
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -4557,9 +4582,9 @@
"devOptional": true
},
"node_modules/@types/react": {
- "version": "17.0.52",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz",
- "integrity": "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==",
+ "version": "18.0.25",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz",
+ "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
@@ -4568,12 +4593,12 @@
}
},
"node_modules/@types/react-dom": {
- "version": "17.0.18",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz",
- "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==",
+ "version": "18.0.9",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz",
+ "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==",
"dev": true,
"dependencies": {
- "@types/react": "^17"
+ "@types/react": "*"
}
},
"node_modules/@types/scheduler": {
@@ -4588,17 +4613,6 @@
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
- "node_modules/@types/styled-components": {
- "version": "5.1.26",
- "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz",
- "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==",
- "dev": true,
- "dependencies": {
- "@types/hoist-non-react-statics": "*",
- "@types/react": "*",
- "csstype": "^3.0.2"
- }
- },
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
@@ -5091,9 +5105,9 @@
"dev": true
},
"node_modules/aria-hidden": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.1.tgz",
- "integrity": "sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.2.tgz",
+ "integrity": "sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==",
"dependencies": {
"tslib": "^2.0.0"
},
@@ -5332,26 +5346,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/babel-plugin-styled-components": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz",
- "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.16.0",
- "@babel/helper-module-imports": "^7.16.0",
- "babel-plugin-syntax-jsx": "^6.18.0",
- "lodash": "^4.17.11",
- "picomatch": "^2.3.0"
- },
- "peerDependencies": {
- "styled-components": ">= 2"
- }
- },
- "node_modules/babel-plugin-syntax-jsx": {
- "version": "6.18.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
- "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw=="
- },
"node_modules/babel-plugin-transform-react-remove-prop-types": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
@@ -5633,14 +5627,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/camelize": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
- "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/caniuse-lite": {
"version": "1.0.30001426",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz",
@@ -6161,24 +6147,6 @@
"node": ">= 8"
}
},
- "node_modules/css-color-keywords": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
- "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/css-to-react-native": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
- "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
- "dependencies": {
- "camelize": "^1.0.0",
- "css-color-keywords": "^1.0.0",
- "postcss-value-parser": "^4.0.2"
- }
- },
"node_modules/csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
@@ -8252,6 +8220,7 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "peer": true,
"dependencies": {
"react-is": "^16.7.0"
}
@@ -8259,7 +8228,8 @@
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "peer": true
},
"node_modules/hosted-git-info": {
"version": "4.1.0",
@@ -10603,6 +10573,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11291,6 +11262,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
"engines": {
"node": ">=8.6"
},
@@ -11410,11 +11382,6 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -11679,44 +11646,36 @@
}
},
"node_modules/react": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
- "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
+ "loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
- "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"dependencies": {
"loose-envify": "^1.1.0",
- "object-assign": "^4.1.1",
- "scheduler": "^0.20.2"
+ "scheduler": "^0.23.0"
},
"peerDependencies": {
- "react": "17.0.2"
+ "react": "^18.2.0"
}
},
"node_modules/react-icons": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz",
- "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==",
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
+ "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==",
"peerDependencies": {
"react": "*"
}
},
- "node_modules/react-is": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
- "peer": true
- },
"node_modules/react-refresh": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
@@ -12385,12 +12344,11 @@
"dev": true
},
"node_modules/scheduler": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
- "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
+ "loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
@@ -12438,11 +12396,6 @@
"node": ">=8"
}
},
- "node_modules/shallowequal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
- "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
- },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -12791,60 +12744,6 @@
"node": ">=4"
}
},
- "node_modules/styled-components": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz",
- "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==",
- "hasInstallScript": true,
- "dependencies": {
- "@babel/helper-module-imports": "^7.0.0",
- "@babel/traverse": "^7.4.5",
- "@emotion/is-prop-valid": "^1.1.0",
- "@emotion/stylis": "^0.8.4",
- "@emotion/unitless": "^0.7.4",
- "babel-plugin-styled-components": ">= 1.12.0",
- "css-to-react-native": "^3.0.0",
- "hoist-non-react-statics": "^3.0.0",
- "shallowequal": "^1.1.0",
- "supports-color": "^5.5.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/styled-components"
- },
- "peerDependencies": {
- "react": ">= 16.8.0",
- "react-dom": ">= 16.8.0",
- "react-is": ">= 16.8.0"
- }
- },
- "node_modules/styled-components/node_modules/@emotion/unitless": {
- "version": "0.7.5",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
- "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
- },
- "node_modules/styled-components/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/styled-components/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/stylis": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
@@ -13793,8 +13692,6 @@
"@emotion/cache": "^11.10.5",
"@emotion/serialize": "^1.1.1",
"@emotion/utils": "^1.2.0",
- "@mantine/core": "^5.6.1",
- "@tippyjs/react": "^4.2.6",
"@tiptap/core": "^2.0.0-beta.182",
"@tiptap/extension-bold": "^2.0.0-beta.28",
"@tiptap/extension-code": "^2.0.0-beta.28",
@@ -13811,22 +13708,14 @@
"@tiptap/extension-strike": "^2.0.0-beta.29",
"@tiptap/extension-text": "^2.0.0-beta.17",
"@tiptap/extension-underline": "^2.0.0-beta.25",
- "@tiptap/react": "^2.0.0-beta.114",
"lodash": "^4.17.21",
"prosemirror-model": "1.18.1",
"prosemirror-state": "1.4.1",
"prosemirror-view": "1.26.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
- "react-icons": "^4.3.1",
- "styled-components": "^5.3.3",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/lodash": "^4.14.179",
- "@types/react": "^17.0.39",
- "@types/react-dom": "^17.0.12",
- "@types/styled-components": "^5.1.24",
"@types/uuid": "^8.3.4",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
@@ -13835,6 +13724,32 @@
"vite": "^3.0.5",
"vite-plugin-eslint": "^1.7.0"
}
+ },
+ "packages/react": {
+ "name": "@blocknote/react",
+ "version": "0.1.2",
+ "dependencies": {
+ "@blocknote/core": "^0.1.2",
+ "@mantine/core": "^5.6.1",
+ "@tippyjs/react": "^4.2.6",
+ "@tiptap/react": "^2.0.0-beta.207",
+ "react-icons": "^4.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^1.0.7",
+ "eslint": "^8.10.0",
+ "eslint-config-react-app": "^7.0.0",
+ "prettier": "^2.7.1",
+ "typescript": "^4.5.4",
+ "vite": "^3.0.5",
+ "vite-plugin-eslint": "^1.7.0"
+ },
+ "peerDependencies": {
+ "react": "^18",
+ "react-dom": "^18"
+ }
}
},
"dependencies": {
@@ -13945,6 +13860,7 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
"integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+ "dev": true,
"requires": {
"@babel/types": "^7.18.6"
}
@@ -15237,8 +15153,6 @@
"@emotion/cache": "^11.10.5",
"@emotion/serialize": "^1.1.1",
"@emotion/utils": "^1.2.0",
- "@mantine/core": "^5.6.1",
- "@tippyjs/react": "^4.2.6",
"@tiptap/core": "^2.0.0-beta.182",
"@tiptap/extension-bold": "^2.0.0-beta.28",
"@tiptap/extension-code": "^2.0.0-beta.28",
@@ -15255,11 +15169,7 @@
"@tiptap/extension-strike": "^2.0.0-beta.29",
"@tiptap/extension-text": "^2.0.0-beta.17",
"@tiptap/extension-underline": "^2.0.0-beta.25",
- "@tiptap/react": "^2.0.0-beta.114",
"@types/lodash": "^4.14.179",
- "@types/react": "^17.0.39",
- "@types/react-dom": "^17.0.12",
- "@types/styled-components": "^5.1.24",
"@types/uuid": "^8.3.4",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
@@ -15268,10 +15178,6 @@
"prosemirror-model": "1.18.1",
"prosemirror-state": "1.4.1",
"prosemirror-view": "1.26.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
- "react-icons": "^4.3.1",
- "styled-components": "^5.3.3",
"typescript": "^4.5.4",
"uuid": "^8.3.2",
"vite": "^3.0.5",
@@ -15282,13 +15188,33 @@
"version": "file:examples/editor",
"requires": {
"@blocknote/core": "^0.1.2",
- "@types/react": "^17.0.39",
- "@types/react-dom": "^17.0.12",
+ "@blocknote/react": "^0.1.2",
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^1.0.7",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^4.5.4",
+ "vite": "^3.0.5",
+ "vite-plugin-eslint": "^1.7.0"
+ }
+ },
+ "@blocknote/react": {
+ "version": "file:packages/react",
+ "requires": {
+ "@blocknote/core": "^0.1.2",
+ "@mantine/core": "^5.6.1",
+ "@tippyjs/react": "^4.2.6",
+ "@tiptap/react": "^2.0.0-beta.207",
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^1.0.7",
+ "eslint": "^8.10.0",
+ "eslint-config-react-app": "^7.0.0",
+ "prettier": "^2.7.1",
+ "react-icons": "^4.3.1",
"typescript": "^4.5.4",
"vite": "^3.0.5",
"vite-plugin-eslint": "^1.7.0"
@@ -15339,14 +15265,6 @@
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
"integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
},
- "@emotion/is-prop-valid": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
- "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
- "requires": {
- "@emotion/memoize": "^0.8.0"
- }
- },
"@emotion/memoize": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
@@ -15385,11 +15303,6 @@
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
"integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
},
- "@emotion/stylis": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
- "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
- },
"@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
@@ -15444,30 +15357,30 @@
}
},
"@floating-ui/core": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz",
- "integrity": "sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA=="
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.2.tgz",
+ "integrity": "sha512-Skfy0YS3NJ5nV9us0uuPN0HDk1Q4edljaOhRBJGDWs9EBa7ZVMYBHRFlhLvvmwEoaIM9BlH6QJFn9/uZg0bACg=="
},
"@floating-ui/dom": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.3.tgz",
- "integrity": "sha512-6H1kwjkOZKabApNtXRiYHvMmYJToJ1DV7rQ3xc/WJpOABhQIOJJOdz2AOejj8X+gcybaFmBpisVTZxBZAM3V0w==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.6.tgz",
+ "integrity": "sha512-kt/tg1oip9OAH1xjCTcx1OpcUpu9rjDw3GKJ/rEhUqhO7QyJWfrHU0DpLTNsH67+JyFL5Kv9X1utsXwKFVtyEQ==",
"requires": {
- "@floating-ui/core": "^1.0.1"
+ "@floating-ui/core": "^1.0.2"
}
},
"@floating-ui/react-dom": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.0.tgz",
- "integrity": "sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.1.tgz",
+ "integrity": "sha512-UW0t1Gi8ikbDRr8cQPVcqIDMBwUEENe5V4wlHWdrJ5egFnRQFBV9JirauTBFI6S8sM1qFUC1i+qa3g87E6CLTw==",
"requires": {
- "@floating-ui/dom": "^1.0.0"
+ "@floating-ui/dom": "^1.0.5"
}
},
"@floating-ui/react-dom-interactions": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.2.tgz",
- "integrity": "sha512-KhF+UN+MVqUx1bG1fe0aAiBl1hbz07Uin6UW70mxwUDhaGpitM16CYvGri1EqGY4hnWK8TQknDSP8iQFOxjhsg==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.3.tgz",
+ "integrity": "sha512-UEHqdnzyoiWNU5az/tAljr9iXFzN18DcvpMqW+/cXz4FEhDEB1ogLtWldOWCujLerPBnSRocADALafelOReMpw==",
"requires": {
"@floating-ui/react-dom": "^1.0.0",
"aria-hidden": "^1.1.3"
@@ -17020,13 +16933,33 @@
"requires": {}
},
"@tiptap/extension-bubble-menu": {
- "version": "2.0.0-beta.199",
- "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.199.tgz",
- "integrity": "sha512-T3K8xoDbX6J62lhIUpclQoW/1XFt7yfI5DCoxtVWUeKaF+pG6kdsB3CPG5C/+AQVlz2jSIJmQuPf8RQFpQs+yg==",
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.209.tgz",
+ "integrity": "sha512-tceZAuDpy3J96uGyCzpJFD3fHABJDTJTq5E0hm+TRQT+eVGVqZI0PE3/4yVFgkCshioTuJq8veMDFcqNsSkKsQ==",
"requires": {
+ "@tiptap/core": "^2.0.0-beta.209",
+ "lodash": "^4.17.21",
"prosemirror-state": "^1.4.1",
"prosemirror-view": "^1.28.2",
"tippy.js": "^6.3.7"
+ },
+ "dependencies": {
+ "@tiptap/core": {
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.0-beta.209.tgz",
+ "integrity": "sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==",
+ "requires": {}
+ },
+ "prosemirror-view": {
+ "version": "1.29.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz",
+ "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==",
+ "requires": {
+ "prosemirror-model": "^1.16.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ }
}
},
"@tiptap/extension-code": {
@@ -17060,16 +16993,6 @@
"prosemirror-dropcursor": "1.5.0"
}
},
- "@tiptap/extension-floating-menu": {
- "version": "2.0.0-beta.199",
- "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.199.tgz",
- "integrity": "sha512-ELjqnNbxW66uqg54zlP2b4EVYUWvT2WvHmeOXALzoLlNzbqUopIl3XNRsvU2Dv1W88C1UjKgnRZIkHKFE1X3CA==",
- "requires": {
- "prosemirror-state": "^1.4.1",
- "prosemirror-view": "^1.28.2",
- "tippy.js": "^6.3.7"
- }
- },
"@tiptap/extension-gapcursor": {
"version": "2.0.0-beta.199",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.199.tgz",
@@ -17141,13 +17064,33 @@
"requires": {}
},
"@tiptap/react": {
- "version": "2.0.0-beta.199",
- "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.199.tgz",
- "integrity": "sha512-AjBtoavcJ7WOoEXdJlrVEdEv6xuI5UFnqB88w8NlORSkWbfQ3uuOm3A0LUZ92/SsBz6NISZbsFahMy0DYgGbIA==",
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.209.tgz",
+ "integrity": "sha512-iuJ+hgaSPETPMX39QpX6e0tZmFAj9azl0qGhNm6NNB1biCehkB4qMfcfwecWFRWVpZKG5UtjJvjJ3UZM167Jlg==",
"requires": {
- "@tiptap/extension-bubble-menu": "^2.0.0-beta.199",
- "@tiptap/extension-floating-menu": "^2.0.0-beta.199",
+ "@tiptap/extension-bubble-menu": "^2.0.0-beta.209",
+ "@tiptap/extension-floating-menu": "^2.0.0-beta.209",
"prosemirror-view": "^1.28.2"
+ },
+ "dependencies": {
+ "@tiptap/extension-floating-menu": {
+ "version": "2.0.0-beta.209",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.209.tgz",
+ "integrity": "sha512-m5ucAguqDxuOvNcsmvuSLcN8TMkbhFmiC6dTJOyaAGjGn6d8Ly6aZh+lEwU228TebM0TKHTp8Xob1cLjV4TGgg==",
+ "requires": {
+ "tippy.js": "^6.3.7"
+ }
+ },
+ "prosemirror-view": {
+ "version": "1.29.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz",
+ "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==",
+ "requires": {
+ "prosemirror-model": "^1.16.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ }
}
},
"@tootallnate/once": {
@@ -17172,16 +17115,6 @@
"integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
"dev": true
},
- "@types/hoist-non-react-statics": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
- "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
- "dev": true,
- "requires": {
- "@types/react": "*",
- "hoist-non-react-statics": "^3.3.0"
- }
- },
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -17236,9 +17169,9 @@
"devOptional": true
},
"@types/react": {
- "version": "17.0.52",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz",
- "integrity": "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==",
+ "version": "18.0.25",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz",
+ "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==",
"devOptional": true,
"requires": {
"@types/prop-types": "*",
@@ -17247,12 +17180,12 @@
}
},
"@types/react-dom": {
- "version": "17.0.18",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz",
- "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==",
+ "version": "18.0.9",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz",
+ "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==",
"dev": true,
"requires": {
- "@types/react": "^17"
+ "@types/react": "*"
}
},
"@types/scheduler": {
@@ -17267,17 +17200,6 @@
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
- "@types/styled-components": {
- "version": "5.1.26",
- "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz",
- "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==",
- "dev": true,
- "requires": {
- "@types/hoist-non-react-statics": "*",
- "@types/react": "*",
- "csstype": "^3.0.2"
- }
- },
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
@@ -17608,9 +17530,9 @@
"dev": true
},
"aria-hidden": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.1.tgz",
- "integrity": "sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.2.tgz",
+ "integrity": "sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==",
"requires": {
"tslib": "^2.0.0"
}
@@ -17787,23 +17709,6 @@
"@babel/helper-define-polyfill-provider": "^0.3.3"
}
},
- "babel-plugin-styled-components": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz",
- "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==",
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.16.0",
- "@babel/helper-module-imports": "^7.16.0",
- "babel-plugin-syntax-jsx": "^6.18.0",
- "lodash": "^4.17.11",
- "picomatch": "^2.3.0"
- }
- },
- "babel-plugin-syntax-jsx": {
- "version": "6.18.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
- "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw=="
- },
"babel-plugin-transform-react-remove-prop-types": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
@@ -18010,11 +17915,6 @@
"quick-lru": "^4.0.1"
}
},
- "camelize": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
- "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="
- },
"caniuse-lite": {
"version": "1.0.30001426",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz",
@@ -18414,21 +18314,6 @@
"which": "^2.0.1"
}
},
- "css-color-keywords": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
- "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="
- },
- "css-to-react-native": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
- "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
- "requires": {
- "camelize": "^1.0.0",
- "css-color-keywords": "^1.0.0",
- "postcss-value-parser": "^4.0.2"
- }
- },
"csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
@@ -19904,6 +19789,7 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "peer": true,
"requires": {
"react-is": "^16.7.0"
},
@@ -19911,7 +19797,8 @@
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "peer": true
}
}
},
@@ -21691,7 +21578,8 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true
},
"object-inspect": {
"version": "1.12.2",
@@ -22190,7 +22078,8 @@
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
},
"pify": {
"version": "5.0.0",
@@ -22263,11 +22152,6 @@
"source-map-js": "^1.0.2"
}
},
- "postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
- },
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -22486,36 +22370,28 @@
"dev": true
},
"react": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
- "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"requires": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
+ "loose-envify": "^1.1.0"
}
},
"react-dom": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
- "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"requires": {
"loose-envify": "^1.1.0",
- "object-assign": "^4.1.1",
- "scheduler": "^0.20.2"
+ "scheduler": "^0.23.0"
}
},
"react-icons": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz",
- "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==",
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
+ "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==",
"requires": {}
},
- "react-is": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
- "peer": true
- },
"react-refresh": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz",
@@ -23011,12 +22887,11 @@
"dev": true
},
"scheduler": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
- "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"requires": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
+ "loose-envify": "^1.1.0"
}
},
"semver": {
@@ -23054,11 +22929,6 @@
"kind-of": "^6.0.2"
}
},
- "shallowequal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
- "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
- },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -23329,43 +23199,6 @@
"through": "^2.3.4"
}
},
- "styled-components": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz",
- "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==",
- "requires": {
- "@babel/helper-module-imports": "^7.0.0",
- "@babel/traverse": "^7.4.5",
- "@emotion/is-prop-valid": "^1.1.0",
- "@emotion/stylis": "^0.8.4",
- "@emotion/unitless": "^0.7.4",
- "babel-plugin-styled-components": ">= 1.12.0",
- "css-to-react-native": "^3.0.0",
- "hoist-non-react-statics": "^3.0.0",
- "shallowequal": "^1.1.0",
- "supports-color": "^5.5.0"
- },
- "dependencies": {
- "@emotion/unitless": {
- "version": "0.7.5",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
- "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
- },
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
- }
- },
"stylis": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
diff --git a/package.json b/package.json
index 5f1970b7f7..3b492f67fb 100644
--- a/package.json
+++ b/package.json
@@ -26,8 +26,8 @@
"postpublish": "rm -rf packages/core/README.md"
},
"overrides": {
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
"prosemirror-view": "1.26.2"
}
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 1cdfbd2953..df48c0880b 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -49,8 +49,6 @@
"@emotion/cache": "^11.10.5",
"@emotion/serialize": "^1.1.1",
"@emotion/utils": "^1.2.0",
- "@mantine/core": "^5.6.1",
- "@tippyjs/react": "^4.2.6",
"@tiptap/core": "^2.0.0-beta.182",
"@tiptap/extension-bold": "^2.0.0-beta.28",
"@tiptap/extension-code": "^2.0.0-beta.28",
@@ -67,26 +65,14 @@
"@tiptap/extension-strike": "^2.0.0-beta.29",
"@tiptap/extension-text": "^2.0.0-beta.17",
"@tiptap/extension-underline": "^2.0.0-beta.25",
- "@tiptap/react": "^2.0.0-beta.114",
"lodash": "^4.17.21",
"prosemirror-model": "1.18.1",
"prosemirror-state": "1.4.1",
"prosemirror-view": "1.26.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
- "react-icons": "^4.3.1",
- "styled-components": "^5.3.3",
"uuid": "^8.3.2"
},
- "overrides": {
- "react-dom": "$react-dom",
- "react": "$react"
- },
"devDependencies": {
"@types/lodash": "^4.14.179",
- "@types/react": "^17.0.39",
- "@types/react-dom": "^17.0.12",
- "@types/styled-components": "^5.1.24",
"@types/uuid": "^8.3.4",
"eslint": "^8.10.0",
"eslint-config-react-app": "^7.0.0",
diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts
new file mode 100644
index 0000000000..5ac5b7ef73
--- /dev/null
+++ b/packages/core/src/BlockNoteEditor.ts
@@ -0,0 +1,54 @@
+import { Editor, EditorOptions } from "@tiptap/core";
+
+// import "./blocknote.css";
+import { getBlockNoteExtensions, UiFactories } from "./BlockNoteExtensions";
+import styles from "./editor.module.css";
+
+export type BlockNoteEditorOptions = EditorOptions & {
+ enableBlockNoteExtensions: boolean;
+ disableHistoryExtension: boolean;
+ uiFactories: UiFactories;
+};
+
+const blockNoteOptions = {
+ enableInputRules: true,
+ enablePasteRules: true,
+ enableCoreExtensions: false,
+};
+
+export class BlockNoteEditor {
+ public readonly tiptapEditor: Editor & { contentComponent: any };
+
+ constructor(options: Partial = {}) {
+ const blockNoteExtensions = getBlockNoteExtensions(
+ options.uiFactories || {}
+ );
+
+ let extensions = options.disableHistoryExtension
+ ? blockNoteExtensions.filter((e) => e.name !== "history")
+ : blockNoteExtensions;
+
+ const tiptapOptions = {
+ ...blockNoteOptions,
+ ...options,
+ extensions:
+ options.enableBlockNoteExtensions === false
+ ? options.extensions
+ : [...(options.extensions || []), ...extensions],
+ editorProps: {
+ attributes: {
+ ...(options.editorProps?.attributes || {}),
+ class: [
+ styles.bnEditor,
+ styles.bnRoot,
+ (options.editorProps?.attributes as any)?.class || "",
+ ].join(" "),
+ },
+ },
+ };
+
+ this.tiptapEditor = new Editor(tiptapOptions) as Editor & {
+ contentComponent: any;
+ };
+ }
+}
diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts
index d6b117146f..ea1ad8e75e 100644
--- a/packages/core/src/BlockNoteExtensions.ts
+++ b/packages/core/src/BlockNoteExtensions.ts
@@ -13,14 +13,20 @@ import Text from "@tiptap/extension-text";
import Underline from "@tiptap/extension-underline";
import { blocks } from "./extensions/Blocks";
import blockStyles from "./extensions/Blocks/nodes/Block.module.css";
-import { BubbleMenuExtension } from "./extensions/BubbleMenu/BubbleMenuExtension";
+import { FormattingToolbarExtension } from "./extensions/FormattingToolbar/FormattingToolbarExtension";
import { DraggableBlocksExtension } from "./extensions/DraggableBlocks/DraggableBlocksExtension";
-import HyperlinkMark from "./extensions/Hyperlinks/HyperlinkMark";
+import HyperlinkMark from "./extensions/HyperlinkToolbar/HyperlinkMark";
import { FixedParagraph } from "./extensions/Paragraph/FixedParagraph";
import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension";
import SlashMenuExtension from "./extensions/SlashMenu";
import { TrailingNode } from "./extensions/TrailingNode/TrailingNodeExtension";
import UniqueID from "./extensions/UniqueID/UniqueID";
+import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes";
+import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes";
+import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes";
+import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes";
+import { Link } from "@tiptap/extension-link";
+import { SlashMenuItem } from "./extensions/SlashMenu/SlashMenuItem";
export const Document = Node.create({
name: "doc",
@@ -28,10 +34,17 @@ export const Document = Node.create({
content: "block+",
});
+export type UiFactories = Partial<{
+ formattingToolbarFactory: FormattingToolbarFactory;
+ hyperlinkToolbarFactory: HyperlinkToolbarFactory;
+ slashMenuFactory: SuggestionsMenuFactory;
+ blockSideMenuFactory: BlockSideMenuFactory;
+}>;
+
/**
* Get all the Tiptap extensions BlockNote is configured with by default
*/
-export const getBlockNoteExtensions = () => {
+export const getBlockNoteExtensions = (uiFactories: UiFactories) => {
const ret: Extensions = [
extensions.ClipboardTextSerializer,
extensions.Commands,
@@ -65,19 +78,51 @@ export const getBlockNoteExtensions = () => {
Italic,
Strike,
Underline,
- HyperlinkMark,
FixedParagraph,
// custom blocks:
...blocks,
- DraggableBlocksExtension,
+
DropCursor.configure({ width: 5, color: "#ddeeff" }),
- BubbleMenuExtension,
History,
// This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command),
// should be handled before Enter handlers in other components like splitListItem
- SlashMenuExtension,
TrailingNode,
];
+
+ if (uiFactories.blockSideMenuFactory) {
+ ret.push(
+ DraggableBlocksExtension.configure({
+ blockSideMenuFactory: uiFactories.blockSideMenuFactory,
+ })
+ );
+ }
+
+ if (uiFactories.formattingToolbarFactory) {
+ ret.push(
+ FormattingToolbarExtension.configure({
+ formattingToolbarFactory: uiFactories.formattingToolbarFactory,
+ })
+ );
+ }
+
+ if (uiFactories.hyperlinkToolbarFactory) {
+ ret.push(
+ HyperlinkMark.configure({
+ hyperlinkToolbarFactory: uiFactories.hyperlinkToolbarFactory,
+ })
+ );
+ } else {
+ ret.push(Link);
+ }
+
+ if (uiFactories.slashMenuFactory) {
+ ret.push(
+ SlashMenuExtension.configure({
+ slashMenuFactory: uiFactories.slashMenuFactory,
+ })
+ );
+ }
+
return ret;
};
diff --git a/packages/core/src/assets/fonts-inter.css b/packages/core/src/assets/fonts-inter.css
new file mode 100644
index 0000000000..a074dfe49d
--- /dev/null
+++ b/packages/core/src/assets/fonts-inter.css
@@ -0,0 +1,92 @@
+/* Generated using https://google-webfonts-helper.herokuapp.com/fonts/inter?subsets=latin */
+
+/* inter-100 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 100;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-100.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-100.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-200 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 200;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-200.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-200.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-300 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 300;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-300.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-regular - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-regular.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-500 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 500;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-500.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-600 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 600;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-600.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-700 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 700;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-700.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-800 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 800;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-800.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* inter-900 - latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 900;
+ src: local(""),
+ url("./inter-v12-latin/inter-v12-latin-900.woff2") format("woff2"),
+ /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url("./inter-v12-latin/inter-v12-latin-900.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
diff --git a/packages/core/src/editor.module.css b/packages/core/src/editor.module.css
index 78fa284a5a..594eb4069a 100644
--- a/packages/core/src/editor.module.css
+++ b/packages/core/src/editor.module.css
@@ -1,3 +1,40 @@
+@import url("./assets/fonts-inter.css");
+
.bnEditor {
outline: none;
}
+
+/*
+bnRoot should be applied to all top-level elements
+
+This includes the Prosemirror editor, but also element such as
+Tippy popups that are appended to document.body directly
+*/
+.bnRoot {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.bnRoot *,
+.bnRoot *::before,
+.bnRoot *::after {
+ -webkit-box-sizing: inherit;
+ -moz-box-sizing: inherit;
+ box-sizing: inherit;
+}
+
+.bnEditor, .dragPreview {
+ /* Define a set of colors to be used throughout the app for consistency
+ see https://atlassian.design/foundations/color for more info */
+ --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
+ --N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */
+
+ font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont,
+ "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
+ "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ color: rgb(60, 65, 73);
+}
diff --git a/packages/core/src/extensions/Blocks/nodes/Block.ts b/packages/core/src/extensions/Blocks/nodes/Block.ts
index aee2aa93f4..c02d140ec3 100644
--- a/packages/core/src/extensions/Blocks/nodes/Block.ts
+++ b/packages/core/src/extensions/Blocks/nodes/Block.ts
@@ -5,16 +5,18 @@ import BlockAttributes from "../BlockAttributes";
import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
import styles from "./Block.module.css";
-import { HeadingContentAttributes } from "./BlockTypes/HeadingBlock/HeadingContent";
-import { ListItemContentAttributes } from "./BlockTypes/ListItemBlock/ListItemContent";
+import { TextContentType } from "./BlockTypes/TextBlock/TextContent";
+import { HeadingContentType } from "./BlockTypes/HeadingBlock/HeadingContent";
+import { ListItemContentType } from "./BlockTypes/ListItemBlock/ListItemContent";
export interface IBlock {
HTMLAttributes: Record
;
}
-export type BlockContentAttributes =
- | HeadingContentAttributes
- | ListItemContentAttributes;
+export type BlockContentType =
+ | TextContentType
+ | HeadingContentType
+ | ListItemContentType;
declare module "@tiptap/core" {
interface Commands {
@@ -25,13 +27,11 @@ declare module "@tiptap/core" {
BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType;
BNSetContentType: (
posInBlock: number,
- type: string,
- attributes?: BlockContentAttributes
+ type: BlockContentType
) => ReturnType;
BNCreateBlockOrSetContentType: (
posInBlock: number,
- type: string,
- attributes?: BlockContentAttributes
+ type: BlockContentType
) => ReturnType;
};
}
@@ -282,23 +282,23 @@ export const Block = Node.create({
return true;
},
- // Changes the block at a given position to a given content type.
+ // Changes the content of a block at a given position to a given type.
BNSetContentType:
- (posInBlock, type, attributes) =>
+ (posInBlock, type) =>
({ state, dispatch }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) {
return false;
}
- const { startPos, endPos } = blockInfo;
+ const { startPos, contentNode } = blockInfo;
if (dispatch) {
state.tr.setBlockType(
startPos + 1,
- endPos - 1,
- state.schema.node(type).type,
- attributes
+ startPos + contentNode.nodeSize + 1,
+ state.schema.node(type.name).type,
+ type.attrs
);
}
@@ -307,7 +307,7 @@ export const Block = Node.create({
// Changes the block at a given position to a given content type if it's empty, otherwise creates a new block of
// that type below it.
BNCreateBlockOrSetContentType:
- (posInBlock, type, attributes) =>
+ (posInBlock, type) =>
({ state, chain }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) {
@@ -320,7 +320,7 @@ export const Block = Node.create({
const oldBlockContentPos = startPos + 1;
return chain()
- .BNSetContentType(posInBlock, type, attributes)
+ .BNSetContentType(posInBlock, type)
.setTextSelection(oldBlockContentPos)
.run();
} else {
@@ -329,7 +329,7 @@ export const Block = Node.create({
return chain()
.BNCreateBlock(newBlockInsertionPos)
- .BNSetContentType(newBlockContentPos, type, attributes)
+ .BNSetContentType(newBlockContentPos, type)
.setTextSelection(newBlockContentPos)
.run();
}
@@ -362,10 +362,9 @@ export const Block = Node.create({
const isTextBlock = contentType.name === "textContent";
if (selectionAtBlockStart && !isTextBlock) {
- return commands.BNSetContentType(
- state.selection.from,
- "textContent"
- );
+ return commands.BNSetContentType(state.selection.from, {
+ name: "textContent",
+ });
}
return false;
@@ -506,41 +505,51 @@ export const Block = Node.create({
"Mod-Alt-1": () =>
this.editor.commands.BNSetContentType(
this.editor.state.selection.anchor,
- "headingContent",
{
- headingLevel: "1",
+ name: "headingContent",
+ attrs: {
+ headingLevel: "1",
+ },
}
),
"Mod-Alt-2": () =>
this.editor.commands.BNSetContentType(
this.editor.state.selection.anchor,
- "headingContent",
{
- headingLevel: "2",
+ name: "headingContent",
+ attrs: {
+ headingLevel: "2",
+ },
}
),
"Mod-Alt-3": () =>
this.editor.commands.BNSetContentType(
this.editor.state.selection.anchor,
- "headingContent",
{
- headingLevel: "3",
+ name: "headingContent",
+ attrs: {
+ headingLevel: "3",
+ },
}
),
"Mod-Shift-7": () =>
this.editor.commands.BNSetContentType(
this.editor.state.selection.anchor,
- "listItemContent",
{
- listItemType: "unordered",
+ name: "listItemContent",
+ attrs: {
+ listItemType: "unordered",
+ },
}
),
"Mod-Shift-8": () =>
this.editor.commands.BNSetContentType(
this.editor.state.selection.anchor,
- "listItemContent",
{
- listItemType: "ordered",
+ name: "listItemContent",
+ attrs: {
+ listItemType: "ordered",
+ },
}
),
};
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts
index 94fadeae7f..d49341c290 100644
--- a/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts
+++ b/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts
@@ -1,8 +1,11 @@
import { InputRule, mergeAttributes, Node } from "@tiptap/core";
import styles from "../../Block.module.css";
-export type HeadingContentAttributes = {
- headingLevel: string;
+export type HeadingContentType = {
+ name: "headingContent";
+ attrs?: {
+ headingLevel: string;
+ };
};
export const HeadingContent = Node.create({
@@ -33,8 +36,11 @@ export const HeadingContent = Node.create({
find: new RegExp(`^(#{${parseInt(level)}})\\s$`),
handler: ({ state, chain, range }) => {
chain()
- .BNSetContentType(state.selection.from, "headingContent", {
- headingLevel: level,
+ .BNSetContentType(state.selection.from, {
+ name: "headingContent",
+ attrs: {
+ headingLevel: level,
+ },
})
// Removes the "#" character(s) used to set the heading.
.deleteRange({ from: range.from, to: range.to });
@@ -49,17 +55,17 @@ export const HeadingContent = Node.create({
{
tag: "h1",
attrs: { headingLevel: "1" },
- node: "block"
+ node: "block",
},
{
tag: "h2",
attrs: { headingLevel: "2" },
- node: "block"
+ node: "block",
},
{
tag: "h3",
attrs: { headingLevel: "3" },
- node: "block"
+ node: "block",
},
];
},
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts
index 21cc0f1eea..08ba9ec6f0 100644
--- a/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts
+++ b/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts
@@ -3,8 +3,11 @@ import { OrderedListItemIndexPlugin } from "./OrderedListItemIndexPlugin";
import { getBlockInfoFromPos } from "../../../helpers/getBlockInfoFromPos";
import styles from "../../Block.module.css";
-export type ListItemContentAttributes = {
- listItemType: string;
+export type ListItemContentType = {
+ name: "listItemContent";
+ attrs?: {
+ listItemType: string;
+ };
};
export const ListItemContent = Node.create({
@@ -42,8 +45,11 @@ export const ListItemContent = Node.create({
find: new RegExp(`^[-+*]\\s$`),
handler: ({ state, chain, range }) => {
chain()
- .BNSetContentType(state.selection.from, "listItemContent", {
- listItemType: "unordered",
+ .BNSetContentType(state.selection.from, {
+ name: "listItemContent",
+ attrs: {
+ listItemType: "unordered",
+ },
})
// Removes the "-", "+", or "*" character used to set the list.
.deleteRange({ from: range.from, to: range.to });
@@ -54,8 +60,11 @@ export const ListItemContent = Node.create({
find: new RegExp(`^1\\.\\s$`),
handler: ({ state, chain, range }) => {
chain()
- .BNSetContentType(state.selection.from, "listItemContent", {
- listItemType: "ordered",
+ .BNSetContentType(state.selection.from, {
+ name: "listItemContent",
+ attrs: {
+ listItemType: "ordered",
+ },
})
// Removes the "1." characters used to set the list.
.deleteRange({ from: range.from, to: range.to });
@@ -83,10 +92,9 @@ export const ListItemContent = Node.create({
// Changes list item block to a text block if both the content is empty.
commands.command(() => {
if (node.textContent.length === 0) {
- return commands.BNSetContentType(
- state.selection.from,
- "textContent"
- );
+ return commands.BNSetContentType(state.selection.from, {
+ name: "textContent",
+ });
}
return false;
@@ -151,7 +159,7 @@ export const ListItemContent = Node.create({
return false;
},
- node: "block"
+ node: "block",
},
];
},
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts
index 99dba95916..e6be56209a 100644
--- a/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts
+++ b/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts
@@ -1,6 +1,11 @@
import { Node } from "@tiptap/core";
import styles from "../../Block.module.css";
+export type TextContentType = {
+ name: "textContent";
+ attrs?: {};
+};
+
export const TextContent = Node.create({
name: "textContent",
group: "blockContent",
diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx
deleted file mode 100644
index 0ee37bcff1..0000000000
--- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { MantineProvider } from "@mantine/core";
-import { Extension } from "@tiptap/core";
-import { PluginKey } from "prosemirror-state";
-import ReactDOM from "react-dom";
-import { BlockNoteTheme } from "../../BlockNoteTheme";
-import rootStyles from "../../root.module.css";
-import { createBubbleMenuPlugin } from "./BubbleMenuPlugin";
-import { BubbleMenu } from "./component/BubbleMenu";
-
-/**
- * The menu that is displayed when selecting a piece of text.
- */
-export const BubbleMenuExtension = Extension.create<{}>({
- name: "BubbleMenuExtension",
-
- addProseMirrorPlugins() {
- const element = document.createElement("div");
- element.className = rootStyles.bnRoot;
- ReactDOM.render(
-
-
- ,
- element
- );
- return [
- createBubbleMenuPlugin({
- editor: this.editor,
- element,
- pluginKey: new PluginKey("BubbleMenuPlugin"),
- tippyOptions: {
- appendTo: document.body,
- },
- }),
- ];
- },
-});
diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts
deleted file mode 100644
index ed87cde122..0000000000
--- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts
+++ /dev/null
@@ -1,245 +0,0 @@
-import {
- Editor,
- isNodeSelection,
- isTextSelection,
- posToDOMRect,
-} from "@tiptap/core";
-import { EditorState, Plugin, PluginKey } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
-import tippy, { Instance, Props } from "tippy.js";
-
-// Same as TipTap bubblemenu plugin, but with these changes:
-// https://github.com/ueberdosis/tiptap/pull/2596/files
-export interface BubbleMenuPluginProps {
- pluginKey: PluginKey | string;
- editor: Editor;
- element: HTMLElement;
- tippyOptions?: Partial;
- shouldShow?:
- | ((props: {
- editor: Editor;
- view: EditorView;
- state: EditorState;
- oldState?: EditorState;
- from: number;
- to: number;
- }) => boolean)
- | null;
-}
-
-export type BubbleMenuViewProps = BubbleMenuPluginProps & {
- view: EditorView;
-};
-
-export class BubbleMenuView {
- public editor: Editor;
-
- public element: HTMLElement;
-
- public view: EditorView;
-
- public preventHide = false;
-
- public preventShow = false;
-
- public tippy: Instance | undefined;
-
- public tippyOptions?: Partial;
-
- public shouldShow: Exclude = ({
- view,
- state,
- from,
- to,
- }) => {
- const { doc, selection } = state;
- const { empty } = selection;
-
- // Sometime check for `empty` is not enough.
- // Doubleclick an empty paragraph returns a node size of 2.
- // So we check also for an empty text size.
- const isEmptyTextBlock =
- !doc.textBetween(from, to).length && isTextSelection(state.selection);
-
- if (!view.hasFocus() || empty || isEmptyTextBlock) {
- return false;
- }
-
- return true;
- };
-
- constructor({
- editor,
- element,
- view,
- tippyOptions = {},
- shouldShow,
- }: BubbleMenuViewProps) {
- this.editor = editor;
- this.element = element;
- this.view = view;
-
- if (shouldShow) {
- this.shouldShow = shouldShow;
- }
-
- this.element.addEventListener("mousedown", this.mousedownHandler, {
- capture: true,
- });
- this.view.dom.addEventListener("mousedown", this.viewMousedownHandler);
- this.view.dom.addEventListener("mouseup", this.viewMouseupHandler);
- this.view.dom.addEventListener("dragstart", this.dragstartHandler);
-
- this.editor.on("focus", this.focusHandler);
- this.editor.on("blur", this.blurHandler);
- this.tippyOptions = tippyOptions;
- // Detaches menu content from its current parent
- this.element.remove();
- this.element.style.visibility = "visible";
- }
-
- mousedownHandler = () => {
- this.preventHide = true;
- };
-
- viewMousedownHandler = () => {
- this.preventShow = true;
- };
-
- viewMouseupHandler = () => {
- this.preventShow = false;
- setTimeout(() => this.update(this.editor.view));
- };
-
- dragstartHandler = () => {
- this.hide();
- };
-
- focusHandler = () => {
- // we use `setTimeout` to make sure `selection` is already updated
- setTimeout(() => this.update(this.editor.view));
- };
-
- blurHandler = ({ event }: { event: FocusEvent }) => {
- if (this.preventHide) {
- this.preventHide = false;
-
- return;
- }
-
- if (
- event?.relatedTarget &&
- this.element.parentNode?.contains(event.relatedTarget as Node)
- ) {
- return;
- }
-
- this.hide();
- };
-
- createTooltip() {
- const { element: editorElement } = this.editor.options;
- const editorIsAttached = !!editorElement.parentElement;
-
- if (this.tippy || !editorIsAttached) {
- return;
- }
-
- this.tippy = tippy(editorElement, {
- duration: 0,
- getReferenceClientRect: null,
- content: this.element,
- interactive: true,
- trigger: "manual",
- placement: "top",
- hideOnClick: "toggle",
- ...this.tippyOptions,
- });
-
- // maybe we have to hide tippy on its own blur event as well
- if (this.tippy.popper.firstChild) {
- (this.tippy.popper.firstChild as HTMLElement).addEventListener(
- "blur",
- (event) => {
- this.blurHandler({ event });
- }
- );
- }
- }
-
- update(view: EditorView, oldState?: EditorState) {
- const { state, composing } = view;
- const { doc, selection } = state;
- const isSame =
- oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
-
- if (composing || isSame) {
- return;
- }
-
- this.createTooltip();
-
- // support for CellSelections
- const { ranges } = selection;
- const from = Math.min(...ranges.map((range) => range.$from.pos));
- const to = Math.max(...ranges.map((range) => range.$to.pos));
-
- const shouldShow = this.shouldShow?.({
- editor: this.editor,
- view,
- state,
- oldState,
- from,
- to,
- });
-
- if (!shouldShow || this.preventShow) {
- this.hide();
-
- return;
- }
-
- this.tippy?.setProps({
- getReferenceClientRect: () => {
- if (isNodeSelection(state.selection)) {
- const node = view.nodeDOM(from) as HTMLElement;
-
- if (node) {
- return node.getBoundingClientRect();
- }
- }
-
- return posToDOMRect(view, from, to);
- },
- });
-
- this.show();
- }
-
- show() {
- this.tippy?.show();
- }
-
- hide() {
- this.tippy?.hide();
- }
-
- destroy() {
- this.tippy?.destroy();
- this.element.removeEventListener("mousedown", this.mousedownHandler, {
- capture: true,
- });
- this.view.dom.removeEventListener("mousedown", this.viewMousedownHandler);
- this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler);
- this.view.dom.removeEventListener("dragstart", this.dragstartHandler);
- this.editor.off("focus", this.focusHandler);
- this.editor.off("blur", this.blurHandler);
- }
-}
-
-export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => {
- return new Plugin({
- key: new PluginKey("BubbleMenuPlugin"),
- view: (view) => new BubbleMenuView({ view, ...options }),
- });
-};
diff --git a/packages/core/src/extensions/BubbleMenu/component/BubbleMenu.tsx b/packages/core/src/extensions/BubbleMenu/component/BubbleMenu.tsx
deleted file mode 100644
index 315d6b967e..0000000000
--- a/packages/core/src/extensions/BubbleMenu/component/BubbleMenu.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-import { Editor } from "@tiptap/core";
-import {
- RiBold,
- RiH1,
- RiH2,
- RiH3,
- RiIndentDecrease,
- RiIndentIncrease,
- RiItalic,
- RiLink,
- RiListOrdered,
- RiListUnordered,
- RiStrikethrough,
- RiText,
- RiUnderline,
-} from "react-icons/ri";
-import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton";
-import { ToolbarDropdown } from "../../../shared/components/toolbar/ToolbarDropdown";
-import { Toolbar } from "../../../shared/components/toolbar/Toolbar";
-import { useEditorForceUpdate } from "../../../shared/hooks/useEditorForceUpdate";
-import { findBlock } from "../../Blocks/helpers/findBlock";
-import { formatKeyboardShortcut } from "../../../utils";
-import LinkToolbarButton from "./LinkToolbarButton";
-import { IconType } from "react-icons";
-import { Node } from "prosemirror-model";
-
-function getBlockName(blockContentNode: Node) {
- if (blockContentNode.type.name === "textContent") {
- return "Text";
- }
-
- if (blockContentNode.type.name === "headingContent") {
- return "Heading " + blockContentNode.attrs["headingLevel"];
- }
-
- if (blockContentNode.type.name === "listItemContent") {
- return blockContentNode.attrs["listItemType"] === "unordered"
- ? "Bullet List"
- : "Numbered List";
- }
-
- return "";
-}
-
-// TODO: add list options, indentation
-export const BubbleMenu = (props: { editor: Editor }) => {
- useEditorForceUpdate(props.editor);
-
- const selectedNode = props.editor.state.selection.$from.node();
- const currentBlockName = getBlockName(selectedNode);
-
- const blockIconMap: Record = {
- Text: RiText,
- "Heading 1": RiH1,
- "Heading 2": RiH2,
- "Heading 3": RiH3,
- "Bullet List": RiListUnordered,
- "Numbered List": RiListOrdered,
- };
-
- return (
-
- {
- // Setting editor focus using a chained command instead causes bubble menu to flicker on click.
- props.editor.view.focus();
- props.editor.commands.BNSetContentType(
- props.editor.state.selection.from,
- "textContent"
- );
- },
- text: "Text",
- icon: RiText,
- isSelected: selectedNode.type.name === "textContent",
- },
- {
- onClick: () => {
- props.editor.view.focus();
- props.editor.commands.BNSetContentType(
- props.editor.state.selection.from,
- "headingContent",
- {
- headingLevel: "1",
- }
- );
- },
- text: "Heading 1",
- icon: RiH1,
- isSelected:
- selectedNode.type.name === "headingContent" &&
- selectedNode.attrs["headingLevel"] === "1",
- },
- {
- onClick: () => {
- props.editor.view.focus();
- props.editor.commands.BNSetContentType(
- props.editor.state.selection.from,
- "headingContent",
- {
- headingLevel: "2",
- }
- );
- },
- text: "Heading 2",
- icon: RiH2,
- isSelected:
- selectedNode.type.name === "headingContent" &&
- selectedNode.attrs["headingLevel"] === "2",
- },
- {
- onClick: () => {
- props.editor.view.focus();
- props.editor.commands.BNSetContentType(
- props.editor.state.selection.from,
- "headingContent",
- {
- headingLevel: "3",
- }
- );
- },
- text: "Heading 3",
- icon: RiH3,
- isSelected:
- selectedNode.type.name === "headingContent" &&
- selectedNode.attrs["headingLevel"] === "3",
- },
- {
- onClick: () => {
- props.editor.view.focus();
- props.editor.commands.BNSetContentType(
- props.editor.state.selection.from,
- "listItemContent",
- {
- listItemType: "unordered",
- }
- );
- },
- text: "Bullet List",
- icon: RiListUnordered,
- isSelected:
- selectedNode.type.name === "listItemContent" &&
- selectedNode.attrs["listItemType"] === "unordered",
- },
- {
- onClick: () => {
- props.editor.view.focus();
- props.editor.commands.BNSetContentType(
- props.editor.state.selection.from,
- "listItemContent",
- {
- listItemType: "ordered",
- }
- );
- },
- text: "Numbered List",
- icon: RiListOrdered,
- isSelected:
- selectedNode.type.name === "listItemContent" &&
- selectedNode.attrs["listItemType"] === "ordered",
- },
- ]}
- />
- {
- // Setting editor focus using a chained command instead causes bubble menu to flicker on click.
- props.editor.view.focus();
- props.editor.commands.toggleBold();
- }}
- isSelected={props.editor.isActive("bold")}
- mainTooltip="Bold"
- secondaryTooltip={formatKeyboardShortcut("Mod+B")}
- icon={RiBold}
- />
- {
- props.editor.view.focus();
- props.editor.commands.toggleItalic();
- }}
- isSelected={props.editor.isActive("italic")}
- mainTooltip="Italic"
- secondaryTooltip={formatKeyboardShortcut("Mod+I")}
- icon={RiItalic}
- />
- {
- props.editor.view.focus();
- props.editor.commands.toggleUnderline();
- }}
- isSelected={props.editor.isActive("underline")}
- mainTooltip="Underline"
- secondaryTooltip={formatKeyboardShortcut("Mod+U")}
- icon={RiUnderline}
- />
- {
- props.editor.view.focus();
- props.editor.commands.toggleStrike();
- }}
- isSelected={props.editor.isActive("strike")}
- mainTooltip="Strike-through"
- secondaryTooltip={formatKeyboardShortcut("Mod+Shift+X")}
- icon={RiStrikethrough}
- />
- {
- props.editor.view.focus();
- props.editor.commands.sinkListItem("block");
- }}
- isDisabled={!props.editor.can().sinkListItem("block")}
- mainTooltip="Indent"
- secondaryTooltip={formatKeyboardShortcut("Tab")}
- icon={RiIndentIncrease}
- />
-
- {
- props.editor.view.focus();
- props.editor.commands.liftListItem("block");
- }}
- isDisabled={
- !props.editor.can().command(({ state }) => {
- const block = findBlock(state.selection);
- if (!block) {
- return false;
- }
- // If the depth is greater than 2 you can lift
- return block.depth > 2;
- })
- }
- mainTooltip="Decrease Indent"
- secondaryTooltip={formatKeyboardShortcut("Shift+Tab")}
- icon={RiIndentDecrease}
- />
-
-
- {/* {
- const comment = this.props.commentStore.createComment();
- props.editor.chain().focus().setComment(comment.id).run();
- }}
- styleDetails={comment}
- /> */}
-
- );
-};
diff --git a/packages/core/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx b/packages/core/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx
deleted file mode 100644
index 9317f9f153..0000000000
--- a/packages/core/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import Tippy from "@tippyjs/react";
-import { Editor } from "@tiptap/core";
-import { useCallback, useState } from "react";
-import {
- ToolbarButton,
- ToolbarButtonProps,
-} from "../../../shared/components/toolbar/ToolbarButton";
-import { EditHyperlinkMenu } from "../../Hyperlinks/menus/EditHyperlinkMenu";
-
-type Props = ToolbarButtonProps & {
- editor: Editor;
-};
-
-/**
- * The link menu button opens a tooltip on click
- */
-export const LinkToolbarButton = (props: Props) => {
- const [creationMenu, setCreationMenu] = useState();
-
- // TODO: review code; does this pattern still make sense?
- const updateCreationMenu = useCallback(() => {
- const onSubmit = (url: string, text: string) => {
- if (url === "") {
- return;
- }
- const mark = props.editor.schema.mark("link", { href: url });
- let { from, to } = props.editor.state.selection;
- props.editor.view.dispatch(
- props.editor.view.state.tr
- .insertText(text, from, to)
- .addMark(from, from + text.length, mark)
- );
- };
-
- // get the currently selected text and url from the document, and use it to
- // create a new creation menu
- const { from, to } = props.editor.state.selection;
- const selectedText = props.editor.state.doc.textBetween(from, to);
- const activeUrl = props.editor.isActive("link")
- ? props.editor.getAttributes("link").href || ""
- : "";
-
- setCreationMenu(
-
- );
- }, [props.editor]);
-
- return (
- {
- updateCreationMenu();
- }}
- interactive={true}
- maxWidth={500}>
-
-
- );
-};
-
-export default LinkToolbarButton;
diff --git a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts
new file mode 100644
index 0000000000..333a763f21
--- /dev/null
+++ b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts
@@ -0,0 +1,20 @@
+import { EditorElement, ElementFactory } from "../../shared/EditorElement";
+
+export type BlockSideMenuStaticParams = {
+ addBlock: () => void;
+ deleteBlock: () => void;
+ blockDragStart: (event: DragEvent) => void;
+ blockDragEnd: () => void;
+ freezeMenu: () => void;
+ unfreezeMenu: () => void;
+};
+
+export type BlockSideMenuDynamicParams = {
+ blockBoundingBox: DOMRect;
+};
+
+export type BlockSideMenu = EditorElement;
+export type BlockSideMenuFactory = ElementFactory<
+ BlockSideMenuStaticParams,
+ BlockSideMenuDynamicParams
+>;
diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts
index e6042402ca..8f4a7af603 100644
--- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts
+++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts
@@ -1,15 +1,33 @@
-import { Extension } from "@tiptap/core";
+import { Editor, Extension } from "@tiptap/core";
+import { BlockSideMenuFactory } from "./BlockSideMenuFactoryTypes";
import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin";
+export type DraggableBlocksOptions = {
+ editor: Editor;
+ blockSideMenuFactory: BlockSideMenuFactory;
+};
+
/**
- * This extension adds a drag handle in front of all nodes with a "data-id" attribute
+ * This extension adds a menu to the side of blocks which features various BlockNote functions such as adding and
+ * removing blocks. More importantly, it adds a drag handle which allows the user to drag and drop blocks.
*
* code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
*/
-export const DraggableBlocksExtension = Extension.create<{}>({
- name: "DraggableBlocksExtension",
- priority: 1000, // Need to be high, in order to hide draghandle when typing slash
- addProseMirrorPlugins() {
- return [createDraggableBlocksPlugin(this.editor)];
- },
-});
+export const DraggableBlocksExtension =
+ Extension.create({
+ name: "DraggableBlocksExtension",
+ priority: 1000, // Need to be high, in order to hide menu when typing slash
+ addProseMirrorPlugins() {
+ if (!this.options.blockSideMenuFactory) {
+ throw new Error(
+ "UI Element factory not defined for DraggableBlocksExtension"
+ );
+ }
+ return [
+ createDraggableBlocksPlugin({
+ editor: this.editor,
+ blockSideMenuFactory: this.options.blockSideMenuFactory,
+ }),
+ ];
+ },
+ });
diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts
similarity index 55%
rename from packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx
rename to packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts
index 7df590bdb9..7fc8d3ba4b 100644
--- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx
+++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts
@@ -1,13 +1,19 @@
-import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
+import { Editor } from "@tiptap/core";
import { Node } from "prosemirror-model";
+import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
import * as pv from "prosemirror-view";
import { EditorView } from "prosemirror-view";
-import ReactDOM from "react-dom";
-import { DragHandle } from "./components/DragHandle";
-import { MantineProvider } from "@mantine/core";
-import { BlockNoteTheme } from "../../BlockNoteTheme";
import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection";
-import { Editor } from "@tiptap/core";
+import { DraggableBlocksOptions } from "./DraggableBlocksExtension";
+import {
+ BlockSideMenu,
+ BlockSideMenuDynamicParams,
+ BlockSideMenuFactory,
+ BlockSideMenuStaticParams,
+} from "./BlockSideMenuFactoryTypes";
+import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos";
+import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension";
+import styles from "../../editor.module.css";
const serializeForClipboard = (pv as any).__serializeForClipboard;
// code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
@@ -162,6 +168,7 @@ function setDragImage(view: EditorView, from: number, to = from) {
// dataTransfer.setDragImage(element) only works if element is attached to the DOM.
dragImageElement = parentClone;
+ dragImageElement.className = styles.dragPreview;
document.body.appendChild(dragImageElement);
}
@@ -218,157 +225,232 @@ function dragStart(e: DragEvent, view: EditorView) {
}
}
-export const createDraggableBlocksPlugin = (editor: Editor) => {
- let dropElement: HTMLElement | undefined;
+export type BlockMenuViewProps = {
+ editor: Editor;
+ blockMenuFactory: BlockSideMenuFactory;
+ horizontalPosAnchoredAtRoot: boolean;
+};
- const WIDTH = 48;
+export class BlockMenuView {
+ editor: Editor;
// When true, the drag handle with be anchored at the same level as root elements
// When false, the drag handle with be just to the left of the element
- const horizontalPosAnchoredAtRoot = true;
+ horizontalPosAnchoredAtRoot: boolean;
- let menuShown = false;
- let addClicked = false;
+ blockMenu: BlockSideMenu;
- const onShow = () => {
- menuShown = true;
- };
- const onHide = () => {
- menuShown = false;
- };
- const onAddClicked = () => {
- addClicked = true;
- };
+ hoveredBlock: HTMLElement | undefined;
- return new Plugin({
- key: new PluginKey("DraggableBlocksPlugin"),
- view(editorView) {
- dropElement = document.createElement("div");
- dropElement.setAttribute("draggable", "true");
- dropElement.style.position = "absolute";
- dropElement.style.height = "24px"; // default height
- document.body.append(dropElement);
-
- dropElement.addEventListener("dragstart", (e) =>
- dragStart(e, editorView)
- );
- dropElement.addEventListener("dragend", () => unsetDragImage());
+ menuOpen = false;
+ menuFrozen = false;
+
+ constructor({
+ editor,
+ blockMenuFactory,
+ horizontalPosAnchoredAtRoot,
+ }: BlockMenuViewProps) {
+ this.editor = editor;
+ this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot;
- return {
- // update(view, prevState) {},
- destroy() {
- if (!dropElement) {
- throw new Error("unexpected");
+ this.blockMenu = blockMenuFactory(this.getStaticParams());
+
+ // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
+ document.body.addEventListener(
+ "mousemove",
+ (event) => {
+ if (this.menuFrozen) {
+ return;
+ }
+
+ // Gets block at mouse cursor's vertical position.
+ const coords = {
+ left: this.editor.view.dom.clientWidth / 2, // take middle of editor
+ top: event.clientY,
+ };
+ const block = getDraggableBlockFromCoords(coords, this.editor.view);
+
+ // Closes the menu if the mouse cursor is beyond the editor vertically.
+ if (!block) {
+ if (this.menuOpen) {
+ this.menuOpen = false;
+ this.blockMenu.hide();
}
- dropElement.parentNode!.removeChild(dropElement);
- dropElement = undefined;
- },
- };
- },
- props: {
- // handleDOMEvents: {
-
- // },
- // handleDOMEvents: {
- // dragend(view, event) {
- // // setTimeout(() => {
- // // let node = document.querySelector(".ProseMirror-hideselection");
- // // if (node) {
- // // node.classList.remove("ProseMirror-hideselection");
- // // }
- // // }, 50);
- // return true;
- // },
- handleKeyDown(_view, _event) {
- if (!dropElement) {
- throw new Error("unexpected");
+
+ return;
+ }
+
+ // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
+ if (
+ this.menuOpen &&
+ this.hoveredBlock?.hasAttribute("data-id") &&
+ this.hoveredBlock?.getAttribute("data-id") === block.id
+ ) {
+ return;
+ }
+
+ // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position.
+ const blockContent = block.node.firstChild as HTMLElement;
+ this.hoveredBlock = blockContent;
+
+ if (!blockContent) {
+ return;
+ }
+
+ // Shows or updates elements.
+ if (!this.menuOpen) {
+ this.menuOpen = true;
+ this.blockMenu.render(this.getDynamicParams(), true);
+ } else {
+ this.blockMenu.render(this.getDynamicParams(), false);
}
- menuShown = false;
- addClicked = false;
- ReactDOM.render(<>>, dropElement);
- return false;
},
- handleDOMEvents: {
- // drag(view, event) {
- // // event.dataTransfer!.;
- // return false;
- // },
- mouseleave(_view, _event: any) {
- if (!dropElement) {
- throw new Error("unexpected");
- }
- // TODO
- // dropElement.style.display = "none";
- return true;
- },
- mousedown(_view, _event: any) {
- if (!dropElement) {
- throw new Error("unexpected");
- }
- menuShown = false;
- addClicked = false;
- ReactDOM.render(<>>, dropElement);
- return false;
- },
- mousemove(view, event: any) {
- if (!dropElement) {
- throw new Error("unexpected");
- }
+ true
+ );
+
+ // Hides and unfreezes the menu whenever the user selects the editor with the mouse or presses a key.
+ // TODO: Better integration with suggestions menu and only editor scope?
+ document.body.addEventListener(
+ "mousedown",
+ (event) => {
+ if (this.blockMenu.element?.contains(event.target as HTMLElement)) {
+ return;
+ }
- if (menuShown || addClicked) {
- // The submenu is open, don't move draghandle
- // Or if the user clicked the add button
- return true;
- }
- const coords = {
- left: view.dom.clientWidth / 2, // take middle of editor
- top: event.clientY,
- };
- const block = getDraggableBlockFromCoords(coords, view);
-
- if (!block) {
- console.warn("Perhaps we should hide element?");
- return true;
- }
+ if (this.menuOpen) {
+ this.menuOpen = false;
+ this.blockMenu.hide();
+ }
+
+ this.menuFrozen = false;
+ },
+ true
+ );
+ document.body.addEventListener(
+ "keydown",
+ () => {
+ if (this.menuOpen) {
+ this.menuOpen = false;
+ this.blockMenu.hide();
+ }
- // I want the dim of the blocks content node
- // because if the block contains other blocks
- // Its dims change, moving the position of the drag handle
- const blockContent = block.node.firstChild as HTMLElement;
+ this.menuFrozen = false;
+ },
+ true
+ );
+ }
- if (!blockContent) {
- return true;
- }
+ destroy() {
+ if (this.menuOpen) {
+ this.menuOpen = false;
+ this.blockMenu.hide();
+ }
+ }
+
+ addBlock() {
+ this.menuOpen = false;
+ this.menuFrozen = true;
+ this.blockMenu.hide();
+
+ const blockBoundingBox = this.hoveredBlock!.getBoundingClientRect();
+
+ const pos = this.editor.view.posAtCoords({
+ left: blockBoundingBox.left,
+ top: blockBoundingBox.top,
+ });
+ if (!pos) {
+ return;
+ }
+
+ const blockInfo = getBlockInfoFromPos(this.editor.state.doc, pos.pos);
+ if (blockInfo === undefined) {
+ return;
+ }
+
+ const { contentNode, endPos } = blockInfo;
+
+ // Creates a new block if current one is not empty for the suggestion menu to open in.
+ if (contentNode.textContent.length !== 0) {
+ const newBlockInsertionPos = endPos + 1;
+ const newBlockContentPos = newBlockInsertionPos + 2;
+
+ this.editor
+ .chain()
+ .BNCreateBlock(newBlockInsertionPos)
+ .BNSetContentType(newBlockContentPos, { name: "textContent" })
+ .setTextSelection(newBlockContentPos)
+ .run();
+ }
- const rect = absoluteRect(blockContent);
- const win = block.node.ownerDocument.defaultView!;
- const dropElementRect = dropElement.getBoundingClientRect();
- const left =
- (horizontalPosAnchoredAtRoot ? getHorizontalAnchor() : rect.left) -
- WIDTH +
- win.pageXOffset;
- rect.top +=
- rect.height / 2 - dropElementRect.height / 2 + win.pageYOffset;
-
- dropElement.style.left = left + "px";
- dropElement.style.top = rect.top + "px";
-
- ReactDOM.render(
-
-
- ,
- dropElement
- );
- return true;
- },
+ // Focuses and activates the suggestion menu.
+ this.editor.view.focus();
+ this.editor.view.dispatch(
+ this.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
+ // TODO import suggestion plugin key
+ activate: true,
+ type: "drag",
+ })
+ );
+ }
+
+ deleteBlock() {
+ this.menuOpen = false;
+ this.blockMenu.hide();
+
+ const blockBoundingBox = this.hoveredBlock!.getBoundingClientRect();
+
+ const pos = this.editor.view.posAtCoords({
+ left: blockBoundingBox.left,
+ top: blockBoundingBox.top,
+ });
+ if (!pos) {
+ return;
+ }
+
+ this.editor.commands.BNDeleteBlock(pos.pos);
+ }
+
+ getStaticParams(): BlockSideMenuStaticParams {
+ return {
+ addBlock: () => this.addBlock(),
+ deleteBlock: () => this.deleteBlock(),
+ blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view),
+ blockDragEnd: () => unsetDragImage(),
+ freezeMenu: () => {
+ this.menuFrozen = true;
},
- },
+ unfreezeMenu: () => {
+ this.menuFrozen = false;
+ },
+ };
+ }
+
+ getDynamicParams(): BlockSideMenuDynamicParams {
+ const blockBoundingBox = this.hoveredBlock!.getBoundingClientRect();
+
+ return {
+ blockBoundingBox: new DOMRect(
+ this.horizontalPosAnchoredAtRoot
+ ? getHorizontalAnchor()
+ : blockBoundingBox.x,
+ blockBoundingBox.y,
+ blockBoundingBox.width,
+ blockBoundingBox.height
+ ),
+ };
+ }
+}
+
+export const createDraggableBlocksPlugin = (
+ options: DraggableBlocksOptions
+) => {
+ return new Plugin({
+ key: new PluginKey("DraggableBlocksPlugin"),
+ view: () =>
+ new BlockMenuView({
+ editor: options.editor,
+ blockMenuFactory: options.blockSideMenuFactory,
+ horizontalPosAnchoredAtRoot: true,
+ }),
});
};
diff --git a/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx b/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx
deleted file mode 100644
index c880b556b8..0000000000
--- a/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { Editor } from "@tiptap/core";
-import { ActionIcon, Menu } from "@mantine/core";
-import { AiOutlinePlus, MdDragIndicator } from "react-icons/all";
-import DragHandleMenu from "./DragHandleMenu";
-import { SlashMenuPluginKey } from "../../SlashMenu/SlashMenuExtension";
-import { getBlockInfoFromPos } from "../../Blocks/helpers/getBlockInfoFromPos";
-
-export const DragHandle = (props: {
- editor: Editor;
- coords: { left: number; top: number };
- onShow?: () => void;
- onHide?: () => void;
- onAddClicked?: () => void;
-}) => {
- const onDelete = () => {
- const pos = props.editor.view.posAtCoords(props.coords);
- if (!pos) {
- return;
- }
-
- props.editor.commands.BNDeleteBlock(pos.pos);
- };
-
- const onAddClick = () => {
- if (props.onAddClicked) {
- props.onAddClicked();
- }
-
- const pos = props.editor.view.posAtCoords(props.coords);
- if (!pos) {
- return;
- }
-
- const blockInfo = getBlockInfoFromPos(props.editor.state.doc, pos.pos);
- if (blockInfo === undefined) {
- return;
- }
-
- const { contentNode, endPos } = blockInfo;
-
- // Creates a new block if current one is not empty for the suggestion menu to open in.
- if (contentNode.textContent.length !== 0) {
- const newBlockInsertionPos = endPos + 1;
- const newBlockContentPos = newBlockInsertionPos + 2;
-
- props.editor
- .chain()
- .BNCreateBlock(newBlockInsertionPos)
- .BNSetContentType(newBlockContentPos, "textContent")
- .setTextSelection(newBlockContentPos)
- .run();
- }
-
- // Focuses and activates the suggestion menu.
- props.editor.view.focus();
- props.editor.view.dispatch(
- props.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
- // TODO import suggestion plugin key
- activate: true,
- type: "drag",
- })
- );
- };
-
- return (
-
-
- {}
-
-
-
- );
-};
diff --git a/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx b/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx
deleted file mode 100644
index 2ccaabfd55..0000000000
--- a/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createStyles, Menu } from "@mantine/core";
-
-type Props = {
- onDelete: () => void;
-};
-
-const DragHandleMenu = (props: Props) => {
- const { classes } = createStyles({ root: {} })(undefined, {
- name: "DragHandleMenu",
- });
-
- return (
-
- Delete
-
- );
-};
-
-export default DragHandleMenu;
diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts
new file mode 100644
index 0000000000..fb63801145
--- /dev/null
+++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts
@@ -0,0 +1,29 @@
+import { Extension } from "@tiptap/core";
+import { PluginKey } from "prosemirror-state";
+import { FormattingToolbarFactory } from "./FormattingToolbarFactoryTypes";
+import { createFormattingToolbarPlugin } from "./FormattingToolbarPlugin";
+
+/**
+ * The menu that is displayed when selecting a piece of text.
+ */
+export const FormattingToolbarExtension = Extension.create<{
+ formattingToolbarFactory: FormattingToolbarFactory;
+}>({
+ name: "FormattingToolbarExtension",
+
+ addProseMirrorPlugins() {
+ if (!this.options.formattingToolbarFactory) {
+ throw new Error(
+ "UI Element factory not defined for FormattingToolbarExtension"
+ );
+ }
+
+ return [
+ createFormattingToolbarPlugin({
+ editor: this.editor,
+ formattingToolbarFactory: this.options.formattingToolbarFactory,
+ pluginKey: new PluginKey("FormattingToolbarPlugin"),
+ }),
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts
new file mode 100644
index 0000000000..fa055803ba
--- /dev/null
+++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts
@@ -0,0 +1,35 @@
+import { EditorElement, ElementFactory } from "../../shared/EditorElement";
+import { BlockContentType } from "../Blocks/nodes/Block";
+
+export type FormattingToolbarStaticParams = {
+ toggleBold: () => void;
+ toggleItalic: () => void;
+ toggleUnderline: () => void;
+ toggleStrike: () => void;
+ setHyperlink: (url: string, text?: string) => void;
+
+ setBlockType: (type: BlockContentType) => void;
+};
+
+export type FormattingToolbarDynamicParams = {
+ boldIsActive: boolean;
+ italicIsActive: boolean;
+ underlineIsActive: boolean;
+ strikeIsActive: boolean;
+ hyperlinkIsActive: boolean;
+ activeHyperlinkUrl: string;
+ activeHyperlinkText: string;
+
+ // BlockContentType is mostly used to set a block's type, so the attr field is optional as block content types have
+ // default values for attributes. However, it means that a block type's attributes field will never be undefined due to
+ // these default values, which the Required type enforces.
+ activeBlockType: Required;
+
+ selectionBoundingBox: DOMRect;
+};
+
+export type FormattingToolbar = EditorElement;
+export type FormattingToolbarFactory = ElementFactory<
+ FormattingToolbarStaticParams,
+ FormattingToolbarDynamicParams
+>;
diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts
new file mode 100644
index 0000000000..61541d1142
--- /dev/null
+++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts
@@ -0,0 +1,308 @@
+import {
+ Editor,
+ isNodeSelection,
+ isTextSelection,
+ posToDOMRect,
+} from "@tiptap/core";
+import { EditorState, Plugin, PluginKey } from "prosemirror-state";
+import { EditorView } from "prosemirror-view";
+import {
+ FormattingToolbar,
+ FormattingToolbarDynamicParams,
+ FormattingToolbarFactory,
+ FormattingToolbarStaticParams,
+} from "./FormattingToolbarFactoryTypes";
+import { BlockContentType } from "../Blocks/nodes/Block";
+
+// Same as TipTap bubblemenu plugin, but with these changes:
+// https://github.com/ueberdosis/tiptap/pull/2596/files
+export interface FormattingToolbarPluginProps {
+ pluginKey: PluginKey;
+ editor: Editor;
+ formattingToolbarFactory: FormattingToolbarFactory;
+ shouldShow?:
+ | ((props: {
+ editor: Editor;
+ view: EditorView;
+ state: EditorState;
+ oldState?: EditorState;
+ from: number;
+ to: number;
+ }) => boolean)
+ | null;
+}
+
+export type FormattingToolbarViewProps = FormattingToolbarPluginProps & {
+ view: EditorView;
+};
+
+export class FormattingToolbarView {
+ public editor: Editor;
+
+ public view: EditorView;
+
+ public formattingToolbar: FormattingToolbar;
+
+ public preventHide = false;
+
+ public preventShow = false;
+
+ public toolbarIsOpen = false;
+
+ public shouldShow: Exclude =
+ ({ view, state, from, to }) => {
+ const { doc, selection } = state;
+ const { empty } = selection;
+
+ // Sometime check for `empty` is not enough.
+ // Doubleclick an empty paragraph returns a node size of 2.
+ // So we check also for an empty text size.
+ const isEmptyTextBlock =
+ !doc.textBetween(from, to).length && isTextSelection(state.selection);
+
+ return !(!view.hasFocus() || empty || isEmptyTextBlock);
+ };
+
+ constructor({
+ editor,
+ formattingToolbarFactory,
+ view,
+ shouldShow,
+ }: FormattingToolbarViewProps) {
+ this.editor = editor;
+ this.view = view;
+
+ this.formattingToolbar = formattingToolbarFactory(this.getStaticParams());
+
+ if (shouldShow) {
+ this.shouldShow = shouldShow;
+ }
+
+ this.view.dom.addEventListener("mousedown", this.viewMousedownHandler);
+ this.view.dom.addEventListener("mouseup", this.viewMouseupHandler);
+ this.view.dom.addEventListener("dragstart", this.dragstartHandler);
+
+ this.editor.on("focus", this.focusHandler);
+ this.editor.on("blur", this.blurHandler);
+ }
+
+ viewMousedownHandler = () => {
+ this.preventShow = true;
+ };
+
+ viewMouseupHandler = () => {
+ this.preventShow = false;
+ setTimeout(() => this.update(this.editor.view));
+ };
+
+ dragstartHandler = () => {
+ this.formattingToolbar.hide();
+ this.toolbarIsOpen = false;
+ };
+
+ focusHandler = () => {
+ // we use `setTimeout` to make sure `selection` is already updated
+ setTimeout(() => this.update(this.editor.view));
+ };
+
+ blurHandler = ({ event }: { event: FocusEvent }) => {
+ if (this.preventHide) {
+ this.preventHide = false;
+
+ return;
+ }
+
+ if (
+ event?.relatedTarget &&
+ this.formattingToolbar.element?.parentNode?.contains(
+ event.relatedTarget as Node
+ )
+ ) {
+ return;
+ }
+
+ if (this.toolbarIsOpen) {
+ this.formattingToolbar.hide();
+ this.toolbarIsOpen = false;
+ }
+ };
+
+ update(view: EditorView, oldState?: EditorState) {
+ const { state, composing } = view;
+ const { doc, selection } = state;
+ const isSame =
+ oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
+
+ if (composing || isSame) {
+ return;
+ }
+
+ // support for CellSelections
+ const { ranges } = selection;
+ const from = Math.min(...ranges.map((range) => range.$from.pos));
+ const to = Math.max(...ranges.map((range) => range.$to.pos));
+
+ const shouldShow = this.shouldShow?.({
+ editor: this.editor,
+ view,
+ state,
+ oldState,
+ from,
+ to,
+ });
+
+ // Checks if menu should be shown.
+ if (
+ !this.toolbarIsOpen &&
+ !this.preventShow &&
+ (shouldShow || this.preventHide)
+ ) {
+ this.formattingToolbar.render(this.getDynamicParams(), true);
+ this.toolbarIsOpen = true;
+
+ // TODO: Is this necessary? Also for other menu plugins.
+ // Listener stops focus moving to the menu on click.
+ this.formattingToolbar.element!.addEventListener("mousedown", (event) =>
+ event.preventDefault()
+ );
+
+ return;
+ }
+
+ // Checks if menu should be updated.
+ if (
+ this.toolbarIsOpen &&
+ !this.preventShow &&
+ (shouldShow || this.preventHide)
+ ) {
+ this.formattingToolbar.render(this.getDynamicParams(), false);
+ return;
+ }
+
+ // Checks if menu should be hidden.
+ if (
+ this.toolbarIsOpen &&
+ !this.preventHide &&
+ (!shouldShow || this.preventShow)
+ ) {
+ this.formattingToolbar.hide();
+ this.toolbarIsOpen = false;
+
+ // Listener stops focus moving to the menu on click.
+ this.formattingToolbar.element!.removeEventListener(
+ "mousedown",
+ (event) => event.preventDefault()
+ );
+
+ return;
+ }
+ }
+
+ destroy() {
+ this.view.dom.removeEventListener("mousedown", this.viewMousedownHandler);
+ this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler);
+ this.view.dom.removeEventListener("dragstart", this.dragstartHandler);
+
+ this.editor.off("focus", this.focusHandler);
+ this.editor.off("blur", this.blurHandler);
+ }
+
+ getSelectionBoundingBox() {
+ const { state } = this.editor.view;
+ const { selection } = state;
+
+ // support for CellSelections
+ const { ranges } = selection;
+ const from = Math.min(...ranges.map((range) => range.$from.pos));
+ const to = Math.max(...ranges.map((range) => range.$to.pos));
+
+ if (isNodeSelection(selection)) {
+ const node = this.editor.view.nodeDOM(from) as HTMLElement;
+
+ if (node) {
+ return node.getBoundingClientRect();
+ }
+ }
+
+ return posToDOMRect(this.editor.view, from, to);
+ }
+
+ getStaticParams(): FormattingToolbarStaticParams {
+ return {
+ toggleBold: () => {
+ this.editor.view.focus();
+ this.editor.commands.toggleBold();
+ },
+ toggleItalic: () => {
+ this.editor.view.focus();
+ this.editor.commands.toggleItalic();
+ },
+ toggleUnderline: () => {
+ this.editor.view.focus();
+ this.editor.commands.toggleUnderline();
+ },
+ toggleStrike: () => {
+ this.editor.view.focus();
+ this.editor.commands.toggleStrike();
+ },
+ setHyperlink: (url: string, text?: string) => {
+ if (url === "") {
+ return;
+ }
+
+ let { from, to } = this.editor.state.selection;
+
+ if (!text) {
+ text = this.editor.state.doc.textBetween(from, to);
+ }
+
+ const mark = this.editor.schema.mark("link", { href: url });
+
+ this.editor.view.dispatch(
+ this.editor.view.state.tr
+ .insertText(text, from, to)
+ .addMark(from, from + text.length, mark)
+ );
+ this.editor.view.focus();
+ },
+ setBlockType: (type: BlockContentType) => {
+ this.editor.view.focus();
+ this.editor.commands.BNSetContentType(
+ this.editor.state.selection.from,
+ type
+ );
+ },
+ };
+ }
+
+ getDynamicParams(): FormattingToolbarDynamicParams {
+ return {
+ boldIsActive: this.editor.isActive("bold"),
+ italicIsActive: this.editor.isActive("italic"),
+ underlineIsActive: this.editor.isActive("underline"),
+ strikeIsActive: this.editor.isActive("strike"),
+ hyperlinkIsActive: this.editor.isActive("link"),
+ activeHyperlinkUrl: this.editor.getAttributes("link").href
+ ? this.editor.getAttributes("link").href
+ : "",
+ activeHyperlinkText: this.editor.state.doc.textBetween(
+ this.editor.state.selection.from,
+ this.editor.state.selection.to
+ ),
+ activeBlockType: {
+ name: this.editor.state.selection.$from.node().type.name,
+ attrs: this.editor.state.selection.$from.node().attrs,
+ } as Required,
+ selectionBoundingBox: this.getSelectionBoundingBox(),
+ };
+ }
+}
+
+export const createFormattingToolbarPlugin = (
+ options: FormattingToolbarPluginProps
+) => {
+ return new Plugin({
+ key: new PluginKey("FormattingToolbarPlugin"),
+ view: (view) => new FormattingToolbarView({ view, ...options }),
+ });
+};
diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts
new file mode 100644
index 0000000000..e61cc46dfd
--- /dev/null
+++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts
@@ -0,0 +1,28 @@
+import { Link } from "@tiptap/extension-link";
+import {
+ createHyperlinkToolbarPlugin,
+ HyperlinkToolbarPluginProps,
+} from "./HyperlinkToolbarPlugin";
+
+/**
+ * This custom link includes a special menu for editing/deleting/opening the link.
+ * The menu will be triggered by hovering over the link with the mouse,
+ * or by moving the cursor inside the link text
+ */
+const Hyperlink = Link.extend({
+ priority: 500,
+ addProseMirrorPlugins() {
+ if (!this.options.hyperlinkToolbarFactory) {
+ throw new Error("UI Element factory not defined for HyperlinkMark");
+ }
+
+ return [
+ ...(this.parent?.() || []),
+ createHyperlinkToolbarPlugin(this.editor, {
+ hyperlinkToolbarFactory: this.options.hyperlinkToolbarFactory,
+ }),
+ ];
+ },
+});
+
+export default Hyperlink;
diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts
new file mode 100644
index 0000000000..77c8cc2b41
--- /dev/null
+++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts
@@ -0,0 +1,19 @@
+import { EditorElement, ElementFactory } from "../../shared/EditorElement";
+
+export type HyperlinkToolbarStaticParams = {
+ editHyperlink: (url: string, text: string) => void;
+ deleteHyperlink: () => void;
+};
+
+export type HyperlinkToolbarDynamicParams = {
+ url: string;
+ text: string;
+
+ boundingBox: DOMRect;
+};
+
+export type HyperlinkToolbar = EditorElement;
+export type HyperlinkToolbarFactory = ElementFactory<
+ HyperlinkToolbarStaticParams,
+ HyperlinkToolbarDynamicParams
+>;
diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts
new file mode 100644
index 0000000000..f5425b6616
--- /dev/null
+++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts
@@ -0,0 +1,251 @@
+import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core";
+import { Mark } from "prosemirror-model";
+import { Plugin, PluginKey } from "prosemirror-state";
+import {
+ HyperlinkToolbar,
+ HyperlinkToolbarDynamicParams,
+ HyperlinkToolbarFactory,
+ HyperlinkToolbarStaticParams,
+} from "./HyperlinkToolbarFactoryTypes";
+const PLUGIN_KEY = new PluginKey("HyperlinkToolbarPlugin");
+
+export type HyperlinkToolbarPluginProps = {
+ hyperlinkToolbarFactory: HyperlinkToolbarFactory;
+};
+
+export type HyperlinkToolbarViewProps = {
+ editor: Editor;
+ hyperlinkToolbarFactory: HyperlinkToolbarFactory;
+};
+
+class HyperlinkToolbarView {
+ editor: Editor;
+
+ hyperlinkToolbar: HyperlinkToolbar;
+
+ menuUpdateTimer: NodeJS.Timeout | undefined;
+ startMenuUpdateTimer: () => void;
+ stopMenuUpdateTimer: () => void;
+
+ mouseHoveredHyperlinkMark: Mark | undefined;
+ mouseHoveredHyperlinkMarkRange: Range | undefined;
+
+ keyboardHoveredHyperlinkMark: Mark | undefined;
+ keyboardHoveredHyperlinkMarkRange: Range | undefined;
+
+ hyperlinkMark: Mark | undefined;
+ hyperlinkMarkRange: Range | undefined;
+
+ constructor({ editor, hyperlinkToolbarFactory }: HyperlinkToolbarViewProps) {
+ this.editor = editor;
+
+ this.hyperlinkToolbar = hyperlinkToolbarFactory(this.getStaticParams());
+
+ this.startMenuUpdateTimer = () => {
+ this.menuUpdateTimer = setTimeout(() => {
+ this.update();
+ }, 250);
+ };
+
+ this.stopMenuUpdateTimer = () => {
+ if (this.menuUpdateTimer) {
+ clearTimeout(this.menuUpdateTimer);
+ this.menuUpdateTimer = undefined;
+ }
+
+ return false;
+ };
+
+ editor.view.dom.addEventListener("mouseover", (event) => {
+ // Resets the hyperlink mark currently hovered by the mouse cursor.
+ this.mouseHoveredHyperlinkMark = undefined;
+ this.mouseHoveredHyperlinkMarkRange = undefined;
+
+ this.stopMenuUpdateTimer();
+
+ if (
+ event.target instanceof HTMLAnchorElement &&
+ event.target.nodeName === "A"
+ ) {
+ // Finds link mark at the hovered element's position to update mouseHoveredHyperlinkMark and
+ // mouseHoveredHyperlinkMarkRange.
+ const hoveredHyperlinkElement = event.target;
+ const posInHoveredHyperlinkMark =
+ editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1;
+ const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve(
+ posInHoveredHyperlinkMark
+ );
+ const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks();
+
+ for (const mark of marksAtPos) {
+ if (mark.type.name === editor.schema.mark("link").type.name) {
+ this.mouseHoveredHyperlinkMark = mark;
+ this.mouseHoveredHyperlinkMarkRange =
+ getMarkRange(
+ resolvedPosInHoveredHyperlinkMark,
+ mark.type,
+ mark.attrs
+ ) || undefined;
+
+ break;
+ }
+ }
+ }
+
+ this.startMenuUpdateTimer();
+
+ return false;
+ });
+ }
+
+ update() {
+ if (!this.editor.view.hasFocus()) {
+ return;
+ }
+
+ // Saves the currently hovered hyperlink mark before it's updated.
+ const prevHyperlinkMark = this.hyperlinkMark;
+
+ // Resets the currently hovered hyperlink mark.
+ this.hyperlinkMark = undefined;
+ this.hyperlinkMarkRange = undefined;
+
+ // Resets the hyperlink mark currently hovered by the keyboard cursor.
+ this.keyboardHoveredHyperlinkMark = undefined;
+ this.keyboardHoveredHyperlinkMarkRange = undefined;
+
+ // Finds link mark at the editor selection's position to update keyboardHoveredHyperlinkMark and
+ // keyboardHoveredHyperlinkMarkRange.
+ if (this.editor.state.selection.empty) {
+ const marksAtPos = this.editor.state.selection.$from.marks();
+
+ for (const mark of marksAtPos) {
+ if (mark.type.name === this.editor.schema.mark("link").type.name) {
+ this.keyboardHoveredHyperlinkMark = mark;
+ this.keyboardHoveredHyperlinkMarkRange =
+ getMarkRange(
+ this.editor.state.selection.$from,
+ mark.type,
+ mark.attrs
+ ) || undefined;
+
+ break;
+ }
+ }
+ }
+
+ if (this.mouseHoveredHyperlinkMark) {
+ this.hyperlinkMark = this.mouseHoveredHyperlinkMark;
+ this.hyperlinkMarkRange = this.mouseHoveredHyperlinkMarkRange;
+ }
+
+ // Keyboard cursor position takes precedence over mouse hovered hyperlink.
+ if (this.keyboardHoveredHyperlinkMark) {
+ this.hyperlinkMark = this.keyboardHoveredHyperlinkMark;
+ this.hyperlinkMarkRange = this.keyboardHoveredHyperlinkMarkRange;
+ }
+
+ if (this.hyperlinkMark) {
+ this.getDynamicParams();
+
+ // Shows menu.
+ if (!prevHyperlinkMark) {
+ this.hyperlinkToolbar.render(this.getDynamicParams(), true);
+
+ this.hyperlinkToolbar.element?.addEventListener(
+ "mouseleave",
+ this.startMenuUpdateTimer
+ );
+ this.hyperlinkToolbar.element?.addEventListener(
+ "mouseenter",
+ this.stopMenuUpdateTimer
+ );
+
+ return;
+ }
+
+ // Updates menu.
+ this.hyperlinkToolbar.render(this.getDynamicParams(), false);
+ }
+
+ // Hides menu.
+ if (!this.hyperlinkMark && prevHyperlinkMark) {
+ this.hyperlinkToolbar.element?.removeEventListener(
+ "mouseleave",
+ this.startMenuUpdateTimer
+ );
+ this.hyperlinkToolbar.element?.removeEventListener(
+ "mouseenter",
+ this.stopMenuUpdateTimer
+ );
+
+ this.hyperlinkToolbar.hide();
+
+ return;
+ }
+ }
+
+ getStaticParams(): HyperlinkToolbarStaticParams {
+ return {
+ editHyperlink: (url: string, text: string) => {
+ const tr = this.editor.view.state.tr.insertText(
+ text,
+ this.hyperlinkMarkRange!.from,
+ this.hyperlinkMarkRange!.to
+ );
+ tr.addMark(
+ this.hyperlinkMarkRange!.from,
+ this.hyperlinkMarkRange!.from + text.length,
+ this.editor.schema.mark("link", { href: url })
+ );
+ this.editor.view.dispatch(tr);
+ this.editor.view.focus();
+
+ this.hyperlinkToolbar.hide();
+ },
+ deleteHyperlink: () => {
+ this.editor.view.dispatch(
+ this.editor.view.state.tr
+ .removeMark(
+ this.hyperlinkMarkRange!.from,
+ this.hyperlinkMarkRange!.to,
+ this.hyperlinkMark!.type
+ )
+ .setMeta("preventAutolink", true)
+ );
+ this.editor.view.focus();
+
+ this.hyperlinkToolbar.hide();
+ },
+ };
+ }
+
+ getDynamicParams(): HyperlinkToolbarDynamicParams {
+ return {
+ url: this.hyperlinkMark!.attrs.href,
+ text: this.editor.view.state.doc.textBetween(
+ this.hyperlinkMarkRange!.from,
+ this.hyperlinkMarkRange!.to
+ ),
+ boundingBox: posToDOMRect(
+ this.editor.view,
+ this.hyperlinkMarkRange!.from,
+ this.hyperlinkMarkRange!.to
+ ),
+ };
+ }
+}
+
+export const createHyperlinkToolbarPlugin = (
+ editor: Editor,
+ options: HyperlinkToolbarPluginProps
+) => {
+ return new Plugin({
+ key: PLUGIN_KEY,
+ view: () =>
+ new HyperlinkToolbarView({
+ editor: editor,
+ hyperlinkToolbarFactory: options.hyperlinkToolbarFactory,
+ }),
+ });
+};
diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx
deleted file mode 100644
index 4431c493d5..0000000000
--- a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Link } from "@tiptap/extension-link";
-import { createHyperlinkMenuPlugin } from "./HyperlinkMenuPlugin";
-
-/**
- * This custom link includes a special menu for editing/deleting/opening the link.
- * The menu will be triggered by hovering over the link with the mouse,
- * or by moving the cursor inside the link text
- */
-const Hyperlink = Link.extend({
- priority: 500,
- addProseMirrorPlugins() {
- return [...(this.parent?.() || []), createHyperlinkMenuPlugin()];
- },
-});
-
-export default Hyperlink;
diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx
deleted file mode 100644
index cd8a62e1ae..0000000000
--- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { MantineProvider } from "@mantine/core";
-import Tippy from "@tippyjs/react";
-import { getMarkRange } from "@tiptap/core";
-import { Mark, ResolvedPos } from "prosemirror-model";
-import { Plugin, PluginKey } from "prosemirror-state";
-import ReactDOM from "react-dom";
-import { BlockNoteTheme } from "../../BlockNoteTheme";
-import { HyperlinkMenu } from "./menus/HyperlinkMenu";
-const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin");
-
-export const createHyperlinkMenuPlugin = () => {
- // as we always use Tippy appendTo(document.body), we can just create an element
- // that we use for ReactDOM, but it isn't used anywhere (except by React internally)
- const fakeRenderTarget = document.createElement("div");
-
- let hoveredLink: HTMLAnchorElement | undefined;
- let menuState: "cursor-based" | "mouse-based" | "hidden" = "hidden";
- let nextTippyKey = 0;
-
- return new Plugin({
- key: PLUGIN_KEY,
- view() {
- return {
- update: async (view, _prevState) => {
- const selection = view.state.selection;
- if (selection.from !== selection.to) {
- // don't show menu when we have an active selection
- if (menuState !== "hidden") {
- menuState = "hidden";
- ReactDOM.render(<>>, fakeRenderTarget);
- }
- return;
- }
-
- let pos: number | undefined;
- let resPos: ResolvedPos | undefined;
- let linkMark: Mark | undefined;
- let basedOnCursorPos = false;
- if (hoveredLink) {
- pos = view.posAtDOM(hoveredLink.firstChild!, 0);
- resPos = view.state.doc.resolve(pos);
- // based on https://github.com/ueberdosis/tiptap/blob/main/packages/core/src/helpers/getMarkRange.ts
- const start = resPos.parent.childAfter(resPos.parentOffset).node;
- linkMark = start?.marks.find((m) => m.type.name.startsWith("link"));
- }
-
- if (
- !linkMark &&
- (view.hasFocus() || menuState === "cursor-based") // prevents re-opening menu after submission. Only open cursor-based menu if editor has focus
- ) {
- // no hovered link mark, try get from cursor position
- pos = selection.from;
- resPos = view.state.doc.resolve(pos);
- const start = resPos.parent.childAfter(resPos.parentOffset).node;
- linkMark = start?.marks.find((m) => m.type.name.startsWith("link"));
- basedOnCursorPos = true;
- }
-
- if (!linkMark || !pos || !resPos) {
- // The mouse-based popup takes care of hiding itself (tippy)
- // Because the cursor-based popup is has "showOnCreate", we want to hide it manually
- // if the cursor moves way
- if (menuState === "cursor-based") {
- menuState = "hidden";
- ReactDOM.render(<>>, fakeRenderTarget);
- }
- return;
- }
-
- const range = getMarkRange(resPos, linkMark.type, linkMark.attrs);
- if (!range) {
- return;
- }
- const text = view.state.doc.textBetween(range.from, range.to);
- const url = linkMark.attrs.href;
-
- const anchorPos = {
- // use the 'median' position of the range
- ...view.coordsAtPos(Math.round((range.from + range.to) / 2)),
- height: 0, // needed to satisfy types
- width: 0,
- };
-
- const foundLinkMark = linkMark; // typescript workaround for event handlers
-
- // A URL has to begin with http(s):// to be interpreted as an absolute path
- const editHandler = (href: string, text: string) => {
- menuState = "hidden";
- ReactDOM.render(<>>, fakeRenderTarget);
-
- // update the mark with new href
- (foundLinkMark as any).attrs = { ...foundLinkMark.attrs, href }; // TODO: invalid assign to attrs
- // insertText actually replaces the range with text
- const tr = view.state.tr.insertText(text, range.from, range.to);
- // the former range.to is no longer in use
- tr.addMark(range.from, range.from + text.length, foundLinkMark);
- view.dispatch(tr);
- };
-
- const removeHandler = () => {
- view.dispatch(
- view.state.tr
- .removeMark(range.from, range.to, foundLinkMark.type)
- .setMeta("preventAutolink", true)
- );
- };
-
- const hyperlinkMenu = (
-
- anchorPos as any}
- content={
-
- }
- onHide={() => {
- nextTippyKey++;
- menuState = "hidden";
- }}
- aria={{ expanded: false }}
- interactive={true}
- interactiveBorder={30}
- triggerTarget={hoveredLink}
- showOnCreate={basedOnCursorPos}
- appendTo={document.body}>
-
-
-
- );
- ReactDOM.render(hyperlinkMenu, fakeRenderTarget);
- menuState = basedOnCursorPos ? "cursor-based" : "mouse-based";
- },
- };
- },
-
- props: {
- handleDOMEvents: {
- // update view when an is hovered over
- mouseover(view, event: any) {
- const newHoveredLink =
- event.target instanceof HTMLAnchorElement &&
- event.target.nodeName === "A"
- ? event.target
- : undefined;
-
- if (newHoveredLink !== hoveredLink) {
- // dispatch a meta transaction to make sure the view gets updated
- hoveredLink = newHoveredLink;
-
- view.dispatch(
- view.state.tr.setMeta(PLUGIN_KEY, { hoveredLinkChanged: true })
- );
- }
- return false;
- },
- },
- },
- });
-};
diff --git a/packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx b/packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx
deleted file mode 100644
index acdaf8c9dc..0000000000
--- a/packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri";
-import { Toolbar } from "../../../shared/components/toolbar/Toolbar";
-import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton";
-
-type HoverHyperlinkMenuProps = {
- url: string;
- edit: () => void;
- remove: () => void;
-};
-
-/**
- * Menu which opens when hovering an existing hyperlink.
- * Provides buttons for editing, opening, and removing the hyperlink.
- */
-export const HoverHyperlinkMenu = (props: HoverHyperlinkMenuProps) => {
- return (
-
-
- Edit Link
-
- {
- window.open(props.url, "_blank");
- }}
- icon={RiExternalLinkFill}
- />
-
-
- );
-};
diff --git a/packages/core/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx b/packages/core/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx
deleted file mode 100644
index 4748334296..0000000000
--- a/packages/core/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useState } from "react";
-import Tippy from "@tippyjs/react";
-import { EditHyperlinkMenu } from "./EditHyperlinkMenu";
-import { HoverHyperlinkMenu } from "./HoverHyperlinkMenu";
-import rootStyles from "../../../root.module.css";
-
-type HyperlinkMenuProps = {
- url: string;
- text: string;
- pos: {
- height: number;
- width: number;
- left: number;
- right: number;
- top: number;
- bottom: number;
- };
- update: (url: string, text: string) => void;
- remove: () => void;
-};
-
-/**
- * Main menu component for the hyperlink extension.
- * Either renders a menu to create/edit a hyperlink, or a menu to interact with it on mouse hover.
- */
-export const HyperlinkMenu = (props: HyperlinkMenuProps) => {
- const [isEditing, setIsEditing] = useState(false);
-
- const editHyperlinkMenu = (
- props.pos as any}
- content={
-
- }
- interactive={true}
- interactiveBorder={30}
- showOnCreate={true}
- trigger={"click"} // so that we don't hide on mouse out
- hideOnClick
- className={rootStyles.bnRoot}
- appendTo={document.body}>
-
-
- );
-
- const hoverHyperlinkMenu = (
- setIsEditing(true)}
- remove={props.remove}
- />
- );
-
- if (isEditing) {
- return editHyperlinkMenu;
- } else {
- return hoverHyperlinkMenu;
- }
-};
diff --git a/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts b/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts
index 4a984d018b..aeb8d21cfb 100644
--- a/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts
+++ b/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts
@@ -1,7 +1,7 @@
import { Editor, Extension } from "@tiptap/core";
import { Node as ProsemirrorNode } from "prosemirror-model";
-import { Decoration, DecorationSet } from "prosemirror-view";
import { Plugin } from "prosemirror-state";
+import { Decoration, DecorationSet } from "prosemirror-view";
import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension";
/**
@@ -78,7 +78,7 @@ export const Placeholder = Extension.create({
}
// If slash menu is of drag type and active, show the filter placeholder
- if (menuState.type === "drag" && menuState.active) {
+ if (menuState?.type === "drag" && menuState?.active) {
classes.push(this.options.isFilterClass);
}
// using widget, didn't work (caret position bug)
diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts
index e2c0c4bafa..35757879f0 100644
--- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts
+++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts
@@ -1,11 +1,13 @@
import { Extension } from "@tiptap/core";
+import { PluginKey } from "prosemirror-state";
import { createSuggestionPlugin } from "../../shared/plugins/suggestion/SuggestionPlugin";
+import { SuggestionsMenuFactory } from "../../shared/plugins/suggestion/SuggestionsMenuFactoryTypes";
import defaultCommands from "./defaultCommands";
import { SlashMenuItem } from "./SlashMenuItem";
-import { PluginKey } from "prosemirror-state";
export type SlashMenuOptions = {
commands: { [key: string]: SlashMenuItem };
+ slashMenuFactory: SuggestionsMenuFactory | undefined;
};
export const SlashMenuPluginKey = new PluginKey("suggestions-slash-commands");
@@ -16,15 +18,21 @@ export const SlashMenuExtension = Extension.create({
addOptions() {
return {
commands: defaultCommands,
+ slashMenuFactory: undefined, // TODO: fix undefined
};
},
addProseMirrorPlugins() {
+ if (!this.options.slashMenuFactory) {
+ throw new Error("UI Element factory not defined for SlashMenuExtension");
+ }
+
return [
createSuggestionPlugin({
pluginKey: SlashMenuPluginKey,
editor: this.editor,
char: "/",
+ suggestionsMenuFactory: this.options.slashMenuFactory!,
items: (query) => {
const commands = [];
diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts b/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts
index 0124ecb76b..576038b894 100644
--- a/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts
+++ b/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts
@@ -1,6 +1,5 @@
import { Editor, Range } from "@tiptap/core";
-import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem";
-import { IconType } from "react-icons";
+import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem";
export type SlashMenuCallback = (editor: Editor, range: Range) => boolean;
@@ -40,7 +39,6 @@ export class SlashMenuItem implements SuggestionItem {
public readonly group: SlashMenuGroups,
public readonly execute: SlashMenuCallback,
public readonly aliases: string[] = [],
- public readonly icon?: IconType,
public readonly hint?: string,
public readonly shortcut?: string
) {
diff --git a/packages/core/src/extensions/SlashMenu/defaultCommands.tsx b/packages/core/src/extensions/SlashMenu/defaultCommands.tsx
index f5024a56cb..d5064b2f55 100644
--- a/packages/core/src/extensions/SlashMenu/defaultCommands.tsx
+++ b/packages/core/src/extensions/SlashMenu/defaultCommands.tsx
@@ -1,12 +1,4 @@
-import {
- RiH1,
- RiH2,
- RiH3,
- RiListOrdered,
- RiListUnordered,
- RiText,
-} from "react-icons/ri";
-import { formatKeyboardShortcut } from "../../utils";
+import { formatKeyboardShortcut } from "../../shared/utils";
import { SlashMenuGroups, SlashMenuItem } from "./SlashMenuItem";
/**
@@ -22,13 +14,15 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
.chain()
.focus()
.deleteRange(range)
- .BNCreateBlockOrSetContentType(range.from, "headingContent", {
- headingLevel: "1",
+ .BNCreateBlockOrSetContentType(range.from, {
+ name: "headingContent",
+ attrs: {
+ headingLevel: "1",
+ },
})
.run();
},
["h", "heading1", "h1"],
- RiH1,
"Used for a top-level heading",
formatKeyboardShortcut("Mod-Alt-1")
),
@@ -42,13 +36,15 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
.chain()
.focus()
.deleteRange(range)
- .BNCreateBlockOrSetContentType(range.from, "headingContent", {
- headingLevel: "2",
+ .BNCreateBlockOrSetContentType(range.from, {
+ name: "headingContent",
+ attrs: {
+ headingLevel: "2",
+ },
})
.run();
},
["h2", "heading2", "subheading"],
- RiH2,
"Used for key sections",
formatKeyboardShortcut("Mod-Alt-2")
),
@@ -62,13 +58,15 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
.chain()
.focus()
.deleteRange(range)
- .BNCreateBlockOrSetContentType(range.from, "headingContent", {
- headingLevel: "3",
+ .BNCreateBlockOrSetContentType(range.from, {
+ name: "headingContent",
+ attrs: {
+ headingLevel: "3",
+ },
})
.run();
},
["h3", "heading3", "subheading"],
- RiH3,
"Used for subsections and group headings",
formatKeyboardShortcut("Mod-Alt-3")
),
@@ -82,13 +80,15 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
.chain()
.focus()
.deleteRange(range)
- .BNCreateBlockOrSetContentType(range.from, "listItemContent", {
- listItemType: "ordered",
+ .BNCreateBlockOrSetContentType(range.from, {
+ name: "listItemContent",
+ attrs: {
+ listItemType: "ordered",
+ },
})
.run();
},
["li", "list", "numberedlist", "numbered list"],
- RiListOrdered,
"Used to display a numbered list",
formatKeyboardShortcut("Mod-Shift-7")
),
@@ -102,13 +102,15 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
.chain()
.focus()
.deleteRange(range)
- .BNCreateBlockOrSetContentType(range.from, "listItemContent", {
- listItemType: "unordered",
+ .BNCreateBlockOrSetContentType(range.from, {
+ name: "listItemContent",
+ attrs: {
+ listItemType: "unordered",
+ },
})
.run();
},
["ul", "list", "bulletlist", "bullet list"],
- RiListUnordered,
"Used to display an unordered list",
formatKeyboardShortcut("Mod-Shift-8")
),
@@ -122,11 +124,10 @@ const defaultCommands: { [key: string]: SlashMenuItem } = {
.chain()
.focus()
.deleteRange(range)
- .BNCreateBlockOrSetContentType(range.from, "textContent")
+ .BNCreateBlockOrSetContentType(range.from, { name: "textContent" })
.run();
},
["p"],
- RiText,
"Used for the body of your document",
formatKeyboardShortcut("Mod-Alt-0")
),
diff --git a/packages/core/src/fonts-inter.css b/packages/core/src/fonts-inter.css
deleted file mode 100644
index 8a2be5de51..0000000000
--- a/packages/core/src/fonts-inter.css
+++ /dev/null
@@ -1,94 +0,0 @@
-/* Generated using https://google-webfonts-helper.herokuapp.com/fonts/inter?subsets=latin */
-
-/* inter-100 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 100;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-100.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-100.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-200 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 200;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-200.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-200.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-300 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 300;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-300.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-regular - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 400;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-regular.woff2")
- format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-regular.woff")
- format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-500 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 500;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-500.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-600 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 600;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-600.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-700 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 700;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-700.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-800 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 800;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-800.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
-/* inter-900 - latin */
-@font-face {
- font-family: "Inter";
- font-style: normal;
- font-weight: 900;
- src: local(""),
- url("./assets/inter-v12-latin/inter-v12-latin-900.woff2") format("woff2"),
- /* Chrome 26+, Opera 23+, Firefox 39+ */
- url("./assets/inter-v12-latin/inter-v12-latin-900.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
-}
diff --git a/packages/core/src/globals.css b/packages/core/src/globals.css
deleted file mode 100644
index 636c561a70..0000000000
--- a/packages/core/src/globals.css
+++ /dev/null
@@ -1,28 +0,0 @@
-@import url("fonts-inter.css");
-
-/* TODO: should not be on root as this changes entire consuming application */
-
-:root {
- /* Define a set of colors to be used throughout the app for consistency
- see https://atlassian.design/foundations/color for more info */
- --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
- --N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */
-
- font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont,
- "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
- "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-
- color: rgb(60, 65, 73);
-}
-
-button {
- font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont,
- "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
- "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-
- color: rgb(60, 65, 73);
-}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 4ec3fed60a..f108f64c36 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,5 +1,9 @@
-import "./globals.css";
-
+export * from "./BlockNoteEditor";
export * from "./BlockNoteExtensions";
-export * from "./EditorContent";
-export * from "./useEditor";
+export type { BlockContentType } from "./extensions/Blocks/nodes/Block";
+export * from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes";
+export * from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes";
+export * from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes";
+export * from "./extensions/SlashMenu/SlashMenuItem";
+export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem";
+export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes";
diff --git a/packages/core/src/root.module.css b/packages/core/src/root.module.css
deleted file mode 100644
index e2f9877bad..0000000000
--- a/packages/core/src/root.module.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-bnRoot should be applied to all top-level elements
-
-This includes the Prosemirror editor, but also element such as
-Tippy popups that are appended to document.body directly
-*/
-.bnRoot {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-.bnRoot *,
-.bnRoot *::before,
-.bnRoot *::after {
- -webkit-box-sizing: inherit;
- -moz-box-sizing: inherit;
- box-sizing: inherit;
-}
diff --git a/packages/core/src/shared/EditorElement.ts b/packages/core/src/shared/EditorElement.ts
new file mode 100644
index 0000000000..57071e2db9
--- /dev/null
+++ b/packages/core/src/shared/EditorElement.ts
@@ -0,0 +1,10 @@
+export type EditorElement
> = {
+ element: HTMLElement | undefined;
+ render: (params: ElementDynamicParams, isHidden: boolean) => void;
+ hide: () => void;
+};
+
+export type ElementFactory<
+ ElementStaticParams extends Record,
+ ElementDynamicParams extends Record
+> = (staticParams: ElementStaticParams) => EditorElement;
diff --git a/packages/core/src/shared/components/tooltip/TooltipContent.module.css b/packages/core/src/shared/components/tooltip/TooltipContent.module.css
deleted file mode 100644
index c687d30ccf..0000000000
--- a/packages/core/src/shared/components/tooltip/TooltipContent.module.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.tooltip {
- color: var(--N40);
- background-color: var(--N800);
- box-shadow: 0 0 10px rgba(253, 254, 255, 0.8),
- 0 0 3px rgba(253, 254, 255, 0.4);
- border-radius: 2px;
- font-size: smaller;
- text-align: center;
- padding: 4px;
-}
-
-.secondaryText {
- font-weight: 400;
- opacity: 0.6;
-}
diff --git a/packages/core/src/shared/components/tooltip/TooltipContent.tsx b/packages/core/src/shared/components/tooltip/TooltipContent.tsx
deleted file mode 100644
index 2a57e841b8..0000000000
--- a/packages/core/src/shared/components/tooltip/TooltipContent.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import styles from "./TooltipContent.module.css";
-
-/**
- * Helper for the tooltip for inline bubble menu buttons.
- *
- * Often used to display a tooltip showing the command name + keyboard shortcut, e.g.:
- *
- * Bold
- * Ctrl+B
- *
- * TODO: maybe use default Tippy styles instead?
- */
-export const TooltipContent = (props: {
- mainTooltip: string;
- secondaryTooltip?: string;
-}) => (
-
-
{props.mainTooltip}
- {props.secondaryTooltip && (
-
{props.secondaryTooltip}
- )}
-
-);
diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts b/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts
index 9f22687fb8..837089e32b 100644
--- a/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts
+++ b/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts
@@ -1,9 +1,7 @@
-import { IconType } from "react-icons";
-
/**
* A generic interface used in all suggestion menus (slash menu, mentions, etc)
*/
-export default interface SuggestionItem {
+export interface SuggestionItem {
/**
* The name of the item
*/
@@ -14,11 +12,6 @@ export default interface SuggestionItem {
*/
groupName: string;
- /**
- * The react icon
- */
- icon?: IconType;
-
hint?: string;
shortcut?: string;
diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx b/packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx
deleted file mode 100644
index 7e712ff48e..0000000000
--- a/packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import { Editor as ReactEditor, ReactRenderer } from "@tiptap/react";
-import { Editor } from "@tiptap/core";
-import tippy, { Instance } from "tippy.js";
-import SuggestionItem from "./SuggestionItem";
-import {
- SuggestionList,
- SuggestionListProps,
-} from "./components/SuggestionList";
-import { BlockNoteTheme } from "../../../BlockNoteTheme";
-import { MantineProvider } from "@mantine/core";
-
-/**
- * The interface that each suggestion renderer should conform to.
- */
-export interface SuggestionRenderer {
- /**
- * Disposes of the suggestion menu.
- */
- onExit?: (props: SuggestionRendererProps) => void;
-
- /**
- * Updates the suggestion menu.
- *
- * This function should be called when the renderer's `props` change,
- * after `onStart` has been called.
- */
- onUpdate?: (props: SuggestionRendererProps) => void;
-
- /**
- * Creates and displays a new suggestion menu popup.
- */
- onStart?: (props: SuggestionRendererProps) => void;
-
- /**
- * Function for handling key events
- */
- onKeyDown?: (event: KeyboardEvent) => boolean;
-
- /**
- * The DOM Element representing the suggestion menu
- */
- getComponent: () => Element | undefined;
-}
-
-export type SuggestionRendererProps = {
- /**
- * Object containing all suggestion items, grouped by their `groupName`.
- */
- groups: {
- [groupName: string]: T[];
- };
-
- /**
- * The total number of suggestion-items.
- */
- count: number;
-
- /**
- * This callback is executed whenever the user selects an item.
- *
- * @param item the selected item
- */
- onSelectItem: (item: T) => void;
-
- /**
- * A function returning the client rect to use as reference for positioning the suggestion menu popup.
- */
- clientRect: (() => DOMRect) | null;
-
- /**
- * This callback is executed when the suggestion menu needs to be closed,
- * e.g. when the user presses escape.
- */
- onClose: () => void;
-};
-
-/**
- * This function creates a SuggestionRenderer based on TipTap's ReactRenderer utility.
- *
- * The resulting renderer can be used to display a suggestion menu containing (grouped) suggestion items.
- *
- * This renderer also takes care of the following key events:
- * - Key up/down, for navigating the suggestion menu (selecting different items)
- * - Enter for picking the currently selected item and closing the menu
- * - Escape to close the menu, without taking action
- *
- * @param editor the TipTap editor
- * @returns the newly constructed SuggestionRenderer
- */
-export default function createRenderer(
- editor: Editor
-): SuggestionRenderer {
- let component: ReactRenderer;
- let popup: Instance[];
- let componentsDisposedOrDisposing = true;
- let selectedIndex = 0;
- let props: SuggestionRendererProps | undefined;
-
- /**
- * Helper function to find out what item corresponds to a certain index.
- *
- * This function might throw an error if the index is invalid,
- * or when this function is not called in the proper environment.
- *
- * @param index the index
- * @returns the item that corresponds to the index
- */
- const itemByIndex = (index: number): T => {
- if (!props) {
- throw new Error("props not set");
- }
- let currentIndex = 0;
- for (const groupName in props.groups) {
- const items = props.groups[groupName];
- const groupSize = items.length;
- // Check if index lies within this group
- if (index < currentIndex + groupSize) {
- return items[index - currentIndex];
- }
- currentIndex += groupSize;
- }
- throw Error("item not found");
- };
-
- return {
- getComponent: () => {
- if (!popup || !popup[0]) {
- return undefined;
- }
- // return the tippy container element, this is used to ensure
- // that click events inside the menu are handled properly.
- return popup[0].reference;
- },
- onStart: (newProps) => {
- props = newProps;
- componentsDisposedOrDisposing = false;
- selectedIndex = 0;
- const componentProps: SuggestionListProps = {
- groups: newProps.groups,
- count: newProps.count,
- onSelectItem: newProps.onSelectItem,
- selectedIndex,
- };
-
- component = new ReactRenderer(
- (props: SuggestionListProps) => (
-
-
-
- ),
- {
- editor: editor as ReactEditor,
- props: componentProps,
- }
- );
-
- popup = tippy("body", {
- getReferenceClientRect: newProps.clientRect,
- appendTo: () => document.body,
- content: component.element,
- showOnCreate: true,
- interactive: true,
- trigger: "manual",
- placement: "bottom-start",
- });
- },
-
- onUpdate: (newProps) => {
- props = newProps;
- if (props.groups !== component.props.groups) {
- // if the set of items is different (e.g.: by typing / searching), reset the selectedIndex to 0
- selectedIndex = 0;
- }
- const componentProps: SuggestionListProps = {
- groups: props.groups,
- count: props.count,
- onSelectItem: props.onSelectItem,
- selectedIndex,
- };
- component.updateProps(componentProps);
-
- popup[0].setProps({
- getReferenceClientRect: props.clientRect,
- });
- },
-
- onKeyDown: (event) => {
- if (!props) {
- return false;
- }
- if (event.key === "ArrowUp") {
- selectedIndex = (selectedIndex + props.count - 1) % props.count;
- component.updateProps({
- selectedIndex,
- });
- return true;
- }
-
- if (event.key === "ArrowDown") {
- selectedIndex = (selectedIndex + 1) % props.count;
- component.updateProps({
- selectedIndex,
- });
- return true;
- }
-
- if (event.key === "Enter") {
- const item = itemByIndex(selectedIndex);
- props.onSelectItem(item);
- return true;
- }
-
- if (event.key === "Escape") {
- props.onClose();
- return true;
- }
- return false;
- },
-
- onExit: (_props) => {
- if (componentsDisposedOrDisposing) {
- return;
- }
- // onExit, first hide tippy popup so it shows fade-out
- // then (after 1 second, actually destroy resources)
- componentsDisposedOrDisposing = true;
- const popupToDestroy = popup[0];
- const componentToDestroy = component;
- popupToDestroy.hide();
- setTimeout(() => {
- popupToDestroy.destroy();
- componentToDestroy.destroy();
- }, 1000);
- },
- };
-}
diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts
index 67b9372c51..adef7aa8b5 100644
--- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts
+++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts
@@ -1,13 +1,15 @@
import { Editor, Range } from "@tiptap/core";
-import { escapeRegExp, groupBy } from "lodash";
-import { Plugin, PluginKey, Selection } from "prosemirror-state";
-import { Decoration, DecorationSet } from "prosemirror-view";
+import { escapeRegExp } from "lodash";
+import { EditorState, Plugin, PluginKey, Selection } from "prosemirror-state";
+import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
import { findBlock } from "../../../extensions/Blocks/helpers/findBlock";
-import SuggestionItem from "./SuggestionItem";
-
-import createRenderer, {
- SuggestionRendererProps,
-} from "./SuggestionListReactRenderer";
+import {
+ SuggestionsMenu,
+ SuggestionsMenuDynamicParams,
+ SuggestionsMenuFactory,
+ SuggestionsMenuStaticParams,
+} from "./SuggestionsMenuFactoryTypes";
+import { SuggestionItem } from "./SuggestionItem";
export type SuggestionPluginOptions = {
/**
@@ -27,6 +29,8 @@ export type SuggestionPluginOptions = {
*/
char: string;
+ suggestionsMenuFactory: SuggestionsMenuFactory;
+
/**
* The callback that gets executed when an item is selected by the user.
*
@@ -44,6 +48,24 @@ export type SuggestionPluginOptions = {
allow?: (props: { editor: Editor; range: Range }) => boolean;
};
+type SuggestionPluginState = {
+ active: boolean;
+ range: Range | null;
+ query: string | null;
+ notFoundCount: number;
+ items: T[];
+ selectedItemIndex: number;
+ type: string;
+ decorationId: string | null;
+};
+
+type SuggestionPluginViewOptions = {
+ editor: Editor;
+ pluginKey: PluginKey;
+ onSelectItem: (props: { item: T; editor: Editor; range: Range }) => void;
+ suggestionsMenuFactory: SuggestionsMenuFactory;
+};
+
export type MenuType = "slash" | "drag";
/**
@@ -85,6 +107,105 @@ export function findCommandBeforeCursor(
};
}
+class SuggestionPluginView {
+ editor: Editor;
+ pluginKey: PluginKey;
+
+ suggestionsMenu: SuggestionsMenu;
+
+ pluginState: SuggestionPluginState;
+ itemCallback: (item: T) => void;
+
+ constructor({
+ editor,
+ pluginKey,
+ onSelectItem: selectItemCallback = () => {},
+ suggestionsMenuFactory,
+ }: SuggestionPluginViewOptions) {
+ this.editor = editor;
+ this.pluginKey = pluginKey;
+
+ this.pluginState = {
+ active: false,
+ range: null,
+ query: null,
+ notFoundCount: 0,
+ items: [],
+ selectedItemIndex: 0,
+ type: "slash",
+ decorationId: null,
+ };
+
+ this.itemCallback = (item: T) =>
+ selectItemCallback({
+ item: item,
+ editor: editor,
+ range: this.pluginState.range as Range,
+ });
+
+ this.suggestionsMenu = suggestionsMenuFactory(this.getStaticParams());
+ }
+
+ update(view: EditorView, prevState: EditorState) {
+ const prev = this.pluginKey.getState(prevState);
+ const next = this.pluginKey.getState(view.state);
+
+ // See how the state changed
+ const started = !prev.active && next.active;
+ const stopped = prev.active && !next.active;
+ // TODO: Currently also true for cases in which an update isn't needed so selected list item index updates still
+ // cause the view to update. May need to be more strict.
+ const changed = prev.active && next.active;
+
+ // Cancel when suggestion isn't active
+ if (!started && !changed && !stopped) {
+ return;
+ }
+
+ this.pluginState = stopped ? prev : next;
+
+ if (stopped) {
+ this.suggestionsMenu.hide();
+
+ // Listener stops focus moving to the menu on click.
+ this.suggestionsMenu.element!.removeEventListener("mousedown", (event) =>
+ event.preventDefault()
+ );
+ }
+
+ if (changed) {
+ this.suggestionsMenu.render(this.getDynamicParams(), false);
+ }
+
+ if (started) {
+ this.suggestionsMenu.render(this.getDynamicParams(), true);
+
+ // Listener stops focus moving to the menu on click.
+ this.suggestionsMenu.element!.addEventListener("mousedown", (event) =>
+ event.preventDefault()
+ );
+ }
+ }
+
+ getStaticParams(): SuggestionsMenuStaticParams {
+ return {
+ itemCallback: (item: T) => this.itemCallback(item),
+ };
+ }
+
+ getDynamicParams(): SuggestionsMenuDynamicParams {
+ const decorationNode = document.querySelector(
+ `[data-decoration-id="${this.pluginState.decorationId}"]`
+ );
+
+ return {
+ items: this.pluginState.items,
+ selectedItemIndex: this.pluginState.selectedItemIndex,
+ queryStartBoundingBox: decorationNode!.getBoundingClientRect(),
+ };
+ }
+}
+
/**
* A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
*
@@ -102,6 +223,7 @@ export function createSuggestionPlugin({
pluginKey,
editor,
char,
+ suggestionsMenuFactory,
onSelectItem: selectItemCallback = () => {},
items = () => [],
}: SuggestionPluginOptions) {
@@ -110,109 +232,42 @@ export function createSuggestionPlugin({
throw new Error("'char' should be a single character");
}
- const renderer = createRenderer(editor);
+ const deactivate = (view: EditorView) => {
+ view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true }));
+ };
// Plugin key is passed in parameter so it can be exported and used in draghandle
return new Plugin({
key: pluginKey,
- filterTransaction(transaction) {
- // prevent blurring when clicking with the mouse inside the popup menu
- const blurMeta = transaction.getMeta("blur");
- if (blurMeta?.event.relatedTarget) {
- const c = renderer.getComponent();
- if (c?.contains(blurMeta.event.relatedTarget)) {
- return false;
- }
- }
- return true;
- },
-
- view() {
- return {
- update: async (view, prevState) => {
- const prev = this.key?.getState(prevState);
- const next = this.key?.getState(view.state);
-
- // See how the state changed
- const started = !prev.active && next.active;
- const stopped = prev.active && !next.active;
- const changed = !started && !stopped && prev.query !== next.query;
-
- // Cancel when suggestion isn't active
- if (!started && !changed && !stopped) {
- return;
- }
-
- const state = stopped ? prev : next;
- const decorationNode = document.querySelector(
- `[data-decoration-id="${state.decorationId}"]`
- );
-
- const groups: { [groupName: string]: T[] } = groupBy(
- state.items,
- "groupName"
- );
-
- const deactivate = () => {
- view.dispatch(
- view.state.tr.setMeta(pluginKey, { deactivate: true })
- );
- };
-
- const rendererProps: SuggestionRendererProps = {
- groups: changed || started ? groups : {},
- count: state.items.length,
- onSelectItem: (item: T) => {
- deactivate();
- selectItemCallback({
- item,
- editor,
- range: state.range,
- });
- },
- // virtual node for popper.js or tippy.js
- // this can be used for building popups without a DOM node
- clientRect: decorationNode
- ? () => decorationNode.getBoundingClientRect()
- : null,
- onClose: () => {
- deactivate();
- renderer.onExit?.(rendererProps);
- },
- };
-
- if (stopped) {
- renderer.onExit?.(rendererProps);
- }
-
- if (changed) {
- renderer.onUpdate?.(rendererProps);
- }
-
- if (started) {
- renderer.onStart?.(rendererProps);
- }
+ view: (view: EditorView) =>
+ new SuggestionPluginView({
+ editor: editor,
+ pluginKey: pluginKey,
+ onSelectItem: (props: { item: T; editor: Editor; range: Range }) => {
+ deactivate(view);
+ selectItemCallback(props);
},
- };
- },
+ suggestionsMenuFactory: suggestionsMenuFactory,
+ }),
state: {
// Initialize the plugin's internal state.
- init() {
+ init(): SuggestionPluginState {
return {
active: false,
- range: {} as any, // TODO
- query: null as string | null,
+ range: null, // TODO
+ query: null,
notFoundCount: 0,
items: [] as T[],
+ selectedItemIndex: 0,
type: "slash",
- decorationId: null as string | null,
+ decorationId: null,
};
},
// Apply changes to the plugin state from a view transaction.
- apply(transaction, prev, _oldState, _newState) {
+ apply(transaction, prev, _oldState, newState) {
const { selection } = transaction;
const next = { ...prev };
@@ -221,6 +276,26 @@ export function createSuggestionPlugin({
return next;
}
+ // Handles transactions created by navigating the menu using the up/down arrow keys.
+ if (
+ transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined
+ ) {
+ let newIndex =
+ transaction.getMeta(pluginKey).selectedItemIndexChanged;
+
+ if (newIndex < 0) {
+ newIndex = prev.items.length - 1;
+ }
+
+ if (newIndex >= prev.items.length) {
+ newIndex = 0;
+ }
+
+ next.selectedItemIndex = newIndex;
+
+ return next;
+ }
+
if (
// only show popup if selection is a blinking cursor
selection.from === selection.to &&
@@ -232,7 +307,7 @@ export function createSuggestionPlugin({
!transaction.getMeta("pointer")
) {
// Reset active state if we just left the previous suggestion range (e.g.: key arrows moving before /)
- if (prev.active && selection.from <= prev.range.from) {
+ if (prev.active && selection.from <= prev.range!.from) {
next.active = false;
} else if (transaction.getMeta(pluginKey)?.activate) {
// Start showing suggestions. activate has been set after typing a "/" (or whatever the specified character is), so let's create the decoration and initialize
@@ -247,13 +322,14 @@ export function createSuggestionPlugin({
next.query = "";
next.active = true;
next.type = transaction.getMeta(pluginKey)?.type;
+ next.selectedItemIndex = 0;
} else if (prev.active) {
// Try to match against where our cursor currently is
// if the type is slash we get the command after the character
// otherwise we get the whole query
const match = findCommandBeforeCursor(
prev.type === "slash" ? char : "",
- selection
+ newState.selection
);
if (!match) {
throw new Error("active but no match (suggestions)");
@@ -263,6 +339,7 @@ export function createSuggestionPlugin({
next.active = true;
next.decorationId = prev.decorationId;
next.query = match.query;
+ next.selectedItemIndex = 0;
}
} else {
next.active = false;
@@ -275,7 +352,7 @@ export function createSuggestionPlugin({
} else {
// Update the "notFoundCount",
// which indicates how many characters have been typed after showing no results
- if (next.range.to > prev.range.to) {
+ if (next.range!.to > prev.range!.to) {
// Text has been entered (selection moved to right), but still no items found, update Count
next.notFoundCount = prev.notFoundCount + 1;
} else {
@@ -293,7 +370,7 @@ export function createSuggestionPlugin({
// Make sure to empty the range if suggestion is inactive
if (!next.active) {
next.decorationId = null;
- next.range = {};
+ next.range = null;
next.query = null;
next.notFoundCount = 0;
next.items = [];
@@ -319,12 +396,51 @@ export function createSuggestionPlugin({
// return true to cancel the original event, as we insert / ourselves
return true;
}
- return false;
+ } else {
+ const { items, range, selectedItemIndex } = pluginKey.getState(
+ view.state
+ );
+
+ if (event.key === "ArrowUp") {
+ view.dispatch(
+ view.state.tr.setMeta(pluginKey, {
+ selectedItemIndexChanged: selectedItemIndex - 1,
+ })
+ );
+ return true;
+ }
+
+ if (event.key === "ArrowDown") {
+ view.dispatch(
+ view.state.tr.setMeta(pluginKey, {
+ selectedItemIndexChanged: selectedItemIndex + 1,
+ })
+ );
+ return true;
+ }
+
+ if (event.key === "Enter") {
+ deactivate(view);
+ selectItemCallback({
+ item: items[selectedItemIndex],
+ editor: editor,
+ range: range,
+ });
+ return true;
+ }
+
+ if (event.key === "Escape") {
+ deactivate(view);
+ return true;
+ }
}
- // pass the key event onto the renderer (to handle arrow keys, enter and escape)
- // return true if the event got handled by the renderer or false otherwise
- return renderer.onKeyDown?.(event) || false;
+ return false;
+ },
+
+ // Hides menu in cases where mouse click does not cause an editor state change.
+ handleClick(view) {
+ deactivate(view);
},
// Setup decorator on the currently active suggestion.
diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts
new file mode 100644
index 0000000000..390a17bb4e
--- /dev/null
+++ b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts
@@ -0,0 +1,21 @@
+import { EditorElement, ElementFactory } from "../../EditorElement";
+import { SuggestionItem } from "./SuggestionItem";
+
+export type SuggestionsMenuStaticParams = {
+ itemCallback: (item: T) => void;
+};
+
+export type SuggestionsMenuDynamicParams = {
+ items: T[];
+ selectedItemIndex: number;
+
+ queryStartBoundingBox: DOMRect;
+};
+
+export type SuggestionsMenu = EditorElement<
+ SuggestionsMenuDynamicParams
+>;
+export type SuggestionsMenuFactory = ElementFactory<
+ SuggestionsMenuStaticParams,
+ SuggestionsMenuDynamicParams
+>;
diff --git a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx b/packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx
deleted file mode 100644
index 716721fdcd..0000000000
--- a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Menu } from "@mantine/core";
-import SuggestionItem from "../SuggestionItem";
-import { SuggestionGroupItem } from "./SuggestionGroupItem";
-
-type SuggestionGroupProps = {
- /**
- * Name of the group
- */
- name: string;
-
- /**
- * The list of items
- */
- items: T[];
-
- /**
- * Index of the selected item in this group; relative to this item group (so 0 refers to the first item in this group)
- * This should be 'undefined' if none of the items in this group are selected
- */
- selectedIndex?: number;
-
- /**
- * Callback that gets executed when an item is clicked on.
- */
- clickItem: (item: T) => void;
-};
-
-export function SuggestionGroup(
- props: SuggestionGroupProps
-) {
- return (
- <>
- {props.name}
- {props.items.map((item, index) => {
- return (
-
- );
- })}
- >
- );
-}
diff --git a/packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx b/packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx
deleted file mode 100644
index 7b4a043126..0000000000
--- a/packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { createStyles, Menu } from "@mantine/core";
-import { SuggestionGroup } from "./SuggestionGroup";
-import SuggestionItem from "../SuggestionItem";
-
-export type SuggestionListProps = {
- // Object containing all suggestion items, grouped by their `groupName`.
- groups: {
- [groupName: string]: T[];
- };
-
- //The total number of suggestion-items
- count: number;
-
- // This callback gets executed whenever an item is clicked on
- onSelectItem: (item: T) => void;
-
- // The index of the item that is currently selected (but not yet clicked on)
- selectedIndex: number;
-};
-
-// Stateless component that renders the suggestion list
-export function SuggestionList(
- props: SuggestionListProps
-) {
- const { classes } = createStyles({ root: {} })(undefined, {
- name: "SuggestionList",
- });
-
- const renderedGroups = [];
-
- let currentGroupIndex = 0;
-
- for (const groupName in props.groups) {
- const items = props.groups[groupName];
-
- renderedGroups.push(
- = currentGroupIndex
- ? props.selectedIndex - currentGroupIndex
- : undefined
- }
- clickItem={props.onSelectItem}>
- );
-
- currentGroupIndex += items.length;
- }
-
- return (
-
-
- // doesn't work well yet, maybe https://github.com/atomiks/tippyjs-react/issues/173
- // We now render the tippy element manually in SuggestionListReactRenderer
- //
- //
- // {renderedGroups.length > 0 ? (
- // renderedGroups
- // ) : (
- //
- // )}
- //
- //
- // }
- // interactive={false}>
- //
- //
- );
-}
diff --git a/packages/core/src/utils.ts b/packages/core/src/shared/utils.ts
similarity index 100%
rename from packages/core/src/utils.ts
rename to packages/core/src/shared/utils.ts
diff --git a/packages/core/src/useEditor.ts b/packages/core/src/useEditor.ts
deleted file mode 100644
index c058ee58e8..0000000000
--- a/packages/core/src/useEditor.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react";
-
-import { DependencyList } from "react";
-import { getBlockNoteExtensions } from "./BlockNoteExtensions";
-import styles from "./editor.module.css";
-import rootStyles from "./root.module.css";
-
-type BlockNoteEditorOptions = EditorOptions & {
- enableBlockNoteExtensions: boolean;
- disableHistoryExtension: boolean;
-};
-
-const blockNoteExtensions = getBlockNoteExtensions();
-
-const blockNoteOptions = {
- enableInputRules: true,
- enablePasteRules: true,
- enableCoreExtensions: false,
-};
-
-/**
- * Main hook for importing a BlockNote editor into a react project
- */
-export const useEditor = (
- options: Partial = {},
- deps: DependencyList = []
-) => {
- const extensions = options.disableHistoryExtension
- ? blockNoteExtensions.filter((e) => e.name !== "history")
- : blockNoteExtensions;
-
- const tiptapOptions = {
- ...blockNoteOptions,
- ...options,
- extensions:
- options.enableBlockNoteExtensions === false
- ? options.extensions
- : [...(options.extensions || []), ...extensions],
- editorProps: {
- attributes: {
- ...(options.editorProps?.attributes || {}),
- class: [
- styles.bnEditor,
- rootStyles.bnRoot,
- (options.editorProps?.attributes as any)?.class || "",
- ].join(" "),
- },
- },
- };
- return useEditorTiptap(tiptapOptions, deps);
-};
diff --git a/packages/core/vite.config.bundled.ts b/packages/core/vite.config.bundled.ts
index 9a6ebab09c..7ca00d3dd8 100644
--- a/packages/core/vite.config.bundled.ts
+++ b/packages/core/vite.config.bundled.ts
@@ -21,8 +21,8 @@ export default defineConfig({
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
- react: "React",
- "react-dom": "ReactDOM",
+ // react: "React",
+ // "react-dom": "ReactDOM",
},
},
},
diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts
index b7bd0b0704..ff80ac1d93 100644
--- a/packages/core/vite.config.ts
+++ b/packages/core/vite.config.ts
@@ -20,10 +20,7 @@ export default defineConfig({
output: {
// Provide global variables to use in the UMD build
// for externalized deps
- globals: {
- react: "React",
- "react-dom": "ReactDOM",
- },
+ globals: {},
},
},
},
diff --git a/packages/react/package.json b/packages/react/package.json
new file mode 100644
index 0000000000..8f22317a2b
--- /dev/null
+++ b/packages/react/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@blocknote/react",
+ "homepage": "https://github.com/yousefed/blocknote",
+ "private": false,
+ "version": "0.1.2",
+ "type": "module",
+ "source": "src/index.ts",
+ "types": "./types/src/index.d.ts",
+ "main": "./dist/blocknote-react.umd.cjs",
+ "module": "./dist/blocknote-react.js",
+ "exports": {
+ ".": {
+ "import": "./dist/blocknote-react.js",
+ "require": "./dist/blocknote-react.umd.cjs"
+ },
+ "./style.css": {
+ "import": "./dist/style.css",
+ "require": "./dist/style.css"
+ }
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release",
+ "preview": "vite preview",
+ "lint": "eslint src --max-warnings 0"
+ },
+ "dependencies": {
+ "@mantine/core": "^5.6.1",
+ "@blocknote/core": "^0.1.2",
+ "@tippyjs/react": "^4.2.6",
+ "@tiptap/react": "^2.0.0-beta.207",
+ "react-icons": "^4.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "eslint": "^8.10.0",
+ "eslint-config-react-app": "^7.0.0",
+ "prettier": "^2.7.1",
+ "typescript": "^4.5.4",
+ "vite": "^3.0.5",
+ "vite-plugin-eslint": "^1.7.0",
+ "@vitejs/plugin-react": "^1.0.7"
+ },
+ "peerDependencies": {
+ "react": "^18",
+ "react-dom": "^18"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ],
+ "rules": {
+ "curly": 1
+ }
+ },
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d"
+}
diff --git a/packages/core/src/BlockNoteTheme.ts b/packages/react/src/BlockNoteTheme.ts
similarity index 88%
rename from packages/core/src/BlockNoteTheme.ts
rename to packages/react/src/BlockNoteTheme.ts
index 61b69bd9fc..bb17186a7a 100644
--- a/packages/core/src/BlockNoteTheme.ts
+++ b/packages/react/src/BlockNoteTheme.ts
@@ -112,7 +112,23 @@ export const BlockNoteTheme: MantineThemeOverride = {
},
}),
},
- SuggestionList: {
+ Tooltip: {
+ styles: (theme) => ({
+ root: {
+ color: theme.colors.brandFinal[2],
+ backgroundColor: theme.colors.brandFinal,
+ border: `1px solid ${theme.colors.brandFinal[1]}`,
+ borderRadius: "6px",
+ boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`,
+ padding: "4px 10px",
+ textAlign: "center",
+ "div ~ div": {
+ color: theme.colors.brandFinal[4],
+ },
+ },
+ }),
+ },
+ SlashMenu: {
styles: (theme) => ({
root: {
// ...theme.other.defaultMenuStyles(theme),
diff --git a/packages/react/src/BlockNoteView.tsx b/packages/react/src/BlockNoteView.tsx
new file mode 100644
index 0000000000..0c7a5e176b
--- /dev/null
+++ b/packages/react/src/BlockNoteView.tsx
@@ -0,0 +1,6 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { EditorContent } from "@tiptap/react";
+
+export function BlockNoteView(props: { editor: BlockNoteEditor | null }) {
+ return ;
+}
diff --git a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx
new file mode 100644
index 0000000000..f284511642
--- /dev/null
+++ b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx
@@ -0,0 +1,57 @@
+import {
+ BlockSideMenu,
+ BlockSideMenuDynamicParams,
+ BlockSideMenuFactory,
+ BlockSideMenuStaticParams,
+} from "@blocknote/core";
+import { BlockSideMenu as ReactSideBlockMenu } from "./components/BlockSideMenu";
+import { createRoot, Root } from "react-dom/client";
+import { MantineProvider } from "@mantine/core";
+import { BlockNoteTheme } from "../BlockNoteTheme";
+import Tippy from "@tippyjs/react";
+
+export const ReactBlockSideMenuFactory: BlockSideMenuFactory = (
+ staticParams: BlockSideMenuStaticParams
+): BlockSideMenu => {
+ // We don't use the document body as a root as it would cause multiple React roots to be created on a single element
+ // if other menu factories do the same.
+ const rootElement = document.createElement("div");
+ // rootElement.className = rootStyles.bnRoot;
+ let root: Root | undefined;
+
+ function getComponent(dynamicParams: BlockSideMenuDynamicParams) {
+ return (
+
+ }
+ duration={0}
+ getReferenceClientRect={() => dynamicParams.blockBoundingBox}
+ hideOnClick={false}
+ interactive={true}
+ offset={[0, 0]}
+ placement={"left"}
+ showOnCreate={true}
+ trigger={"manual"}
+ />
+
+ );
+ }
+
+ return {
+ element: rootElement,
+ render: (dynamicParams: BlockSideMenuDynamicParams, isHidden: boolean) => {
+ if (isHidden) {
+ document.body.appendChild(rootElement);
+ root = createRoot(rootElement);
+ }
+
+ root!.render(getComponent(dynamicParams));
+ },
+ hide: () => {
+ root!.unmount();
+
+ rootElement.remove();
+ },
+ };
+};
diff --git a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx
new file mode 100644
index 0000000000..9ea03e45e0
--- /dev/null
+++ b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx
@@ -0,0 +1,69 @@
+import { AiOutlinePlus, MdDragIndicator } from "react-icons/all";
+import { ActionIcon, createStyles, Group, Menu } from "@mantine/core";
+import { useEffect, useRef } from "react";
+
+export type BlockSideMenuProps = {
+ addBlock: () => void;
+ deleteBlock: () => void;
+ blockDragStart: (event: DragEvent) => void;
+ blockDragEnd: () => void;
+ freezeMenu: () => void;
+ unfreezeMenu: () => void;
+};
+
+export const BlockSideMenu = (props: BlockSideMenuProps) => {
+ const { classes } = createStyles({ root: {} })(undefined, {
+ name: "DragHandleMenu",
+ });
+
+ const dragHandleRef = useRef(null);
+
+ useEffect(() => {
+ const dragHandle = dragHandleRef.current;
+
+ if (dragHandle instanceof HTMLDivElement) {
+ dragHandle.addEventListener("dragstart", props.blockDragStart);
+ dragHandle.addEventListener("dragend", props.blockDragEnd);
+
+ return () => {
+ dragHandle.removeEventListener("dragstart", props.blockDragStart);
+ dragHandle.removeEventListener("dragend", props.blockDragEnd);
+ };
+ }
+
+ return;
+ }, [props.blockDragEnd, props.blockDragStart]);
+
+ return (
+
+
+ {
+ {
+ props.addBlock();
+ }}
+ />
+ }
+
+
+
+ );
+};
diff --git a/packages/core/src/EditorContent.tsx b/packages/react/src/Editor/EditorContent.tsx
similarity index 100%
rename from packages/core/src/EditorContent.tsx
rename to packages/react/src/Editor/EditorContent.tsx
diff --git a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx
new file mode 100644
index 0000000000..14a57ef4b7
--- /dev/null
+++ b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx
@@ -0,0 +1,62 @@
+import { createRoot, Root } from "react-dom/client";
+import {
+ FormattingToolbar,
+ FormattingToolbarFactory,
+ FormattingToolbarStaticParams,
+ FormattingToolbarDynamicParams,
+} from "@blocknote/core";
+import { MantineProvider } from "@mantine/core";
+import Tippy from "@tippyjs/react";
+import { FormattingToolbar as ReactFormattingToolbar } from "./components/FormattingToolbar";
+import { BlockNoteTheme } from "../BlockNoteTheme";
+// import rootStyles from "../../../core/src/root.module.css";
+
+export const ReactFormattingToolbarFactory: FormattingToolbarFactory = (
+ staticParams: FormattingToolbarStaticParams
+): FormattingToolbar => {
+ // We don't use the document body as a root as it would cause multiple React roots to be created on a single element
+ // if other menu factories do the same.
+ const rootElement = document.createElement("div");
+ // rootElement.className = rootStyles.bnRoot;
+ let root: Root | undefined;
+
+ function getComponent(dynamicParams: FormattingToolbarDynamicParams) {
+ return (
+
+
+ }
+ duration={0}
+ getReferenceClientRect={() => dynamicParams.selectionBoundingBox}
+ hideOnClick={false}
+ interactive={true}
+ placement={"top-start"}
+ showOnCreate={true}
+ trigger={"manual"}
+ />
+
+ );
+ }
+
+ return {
+ element: rootElement,
+ render: (
+ dynamicParams: FormattingToolbarDynamicParams,
+ isHidden: boolean
+ ) => {
+ if (isHidden) {
+ document.body.appendChild(rootElement);
+ root = createRoot(rootElement);
+ }
+
+ root!.render(getComponent(dynamicParams));
+ },
+ hide: () => {
+ root!.unmount();
+
+ rootElement.remove();
+ },
+ };
+};
diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
new file mode 100644
index 0000000000..485fd4b575
--- /dev/null
+++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
@@ -0,0 +1,259 @@
+import {
+ RiBold,
+ RiH1,
+ RiH2,
+ RiH3,
+ RiIndentDecrease,
+ RiIndentIncrease,
+ RiItalic,
+ RiLink,
+ RiListOrdered,
+ RiListUnordered,
+ RiStrikethrough,
+ RiText,
+ RiUnderline,
+} from "react-icons/ri";
+import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton";
+import { ToolbarDropdown } from "../../SharedComponents/Toolbar/components/ToolbarDropdown";
+import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar";
+import { formatKeyboardShortcut } from "../../utils";
+import LinkToolbarButton from "./LinkToolbarButton";
+import { BlockContentType } from "@blocknote/core";
+
+export type FormattingToolbarProps = {
+ boldIsActive: boolean;
+ toggleBold: () => void;
+ italicIsActive: boolean;
+ toggleItalic: () => void;
+ underlineIsActive: boolean;
+ toggleUnderline: () => void;
+ strikeIsActive: boolean;
+ toggleStrike: () => void;
+ hyperlinkIsActive: boolean;
+ activeHyperlinkUrl: string;
+ activeHyperlinkText: string;
+ setHyperlink: (url: string, text?: string) => void;
+
+ activeBlockType: Required;
+ setBlockType: (type: BlockContentType) => void;
+};
+
+// TODO: add list options, indentation
+export const FormattingToolbar = (props: FormattingToolbarProps) => {
+ const getActiveMarks = () => {
+ const activeMarks = new Set();
+
+ props.boldIsActive && activeMarks.add("bold");
+ props.italicIsActive && activeMarks.add("italic");
+ props.underlineIsActive && activeMarks.add("underline");
+ props.strikeIsActive && activeMarks.add("strike");
+ props.hyperlinkIsActive && activeMarks.add("link");
+
+ return activeMarks;
+ };
+
+ const getActiveBlock = () => {
+ if (props.activeBlockType.name === "headingContent") {
+ if (props.activeBlockType.attrs["headingLevel"] === "1") {
+ return {
+ text: "Heading 1",
+ icon: RiH1,
+ };
+ }
+
+ if (props.activeBlockType.attrs["headingLevel"] === "2") {
+ return {
+ text: "Heading 2",
+ icon: RiH2,
+ };
+ }
+
+ if (props.activeBlockType.attrs["headingLevel"] === "3") {
+ return {
+ text: "Heading 3",
+ icon: RiH3,
+ };
+ }
+ }
+
+ if (props.activeBlockType.name === "listItemContent") {
+ if (props.activeBlockType.attrs["listItemType"] === "unordered") {
+ return {
+ text: "Bullet List",
+ icon: RiListUnordered,
+ };
+ } else {
+ return {
+ text: "Ordered List",
+ icon: RiListOrdered,
+ };
+ }
+ }
+
+ return {
+ text: "Text",
+ icon: RiText,
+ };
+ };
+
+ const activeMarks = getActiveMarks();
+ const activeBlock = getActiveBlock();
+
+ return (
+
+ props.setBlockType({ name: "textContent" }),
+ text: "Text",
+ icon: RiText,
+ isSelected: props.activeBlockType.name === "textContent",
+ },
+ {
+ onClick: () =>
+ props.setBlockType({
+ name: "headingContent",
+ attrs: { headingLevel: "1" },
+ }),
+ text: "Heading 1",
+ icon: RiH1,
+ isSelected:
+ props.activeBlockType.name === "headingContent" &&
+ props.activeBlockType.attrs["headingLevel"] === "1",
+ },
+ {
+ onClick: () =>
+ props.setBlockType({
+ name: "headingContent",
+ attrs: { headingLevel: "2" },
+ }),
+ text: "Heading 2",
+ icon: RiH2,
+ isSelected:
+ props.activeBlockType.name === "headingContent" &&
+ props.activeBlockType.attrs["headingLevel"] === "2",
+ },
+ {
+ onClick: () =>
+ props.setBlockType({
+ name: "headingContent",
+ attrs: { headingLevel: "3" },
+ }),
+ text: "Heading 3",
+ icon: RiH3,
+ isSelected:
+ props.activeBlockType.name === "headingContent" &&
+ props.activeBlockType.attrs["headingLevel"] === "3",
+ },
+ {
+ onClick: () =>
+ props.setBlockType({
+ name: "listItemContent",
+ attrs: { listItemType: "unordered" },
+ }),
+ text: "Bullet List",
+ icon: RiListUnordered,
+ isSelected:
+ props.activeBlockType.name === "listItemContent" &&
+ props.activeBlockType.attrs["listItemType"] === "unordered",
+ },
+ {
+ onClick: () =>
+ props.setBlockType({
+ name: "listItemContent",
+ attrs: { listItemType: "ordered" },
+ }),
+ text: "Numbered List",
+ icon: RiListOrdered,
+ isSelected:
+ props.activeBlockType.name === "listItemContent" &&
+ props.activeBlockType.attrs["listItemType"] === "ordered",
+ },
+ ]}
+ />
+
+
+
+
+ {
+ // props.editor.view.focus();
+ // props.editor.commands.sinkListItem("block");
+ }}
+ isDisabled={
+ // !props.editor.can().sinkListItem("block")
+ true
+ }
+ mainTooltip="Indent"
+ secondaryTooltip={formatKeyboardShortcut("Tab")}
+ icon={RiIndentIncrease}
+ />
+
+ {
+ // props.editor.view.focus();
+ // props.editor.commands.liftListItem("block");
+ }}
+ isDisabled={
+ // !props.editor.can().command(({ state }) => {
+ // const block = findBlock(state.selection);
+ // if (!block) {
+ // return false;
+ // }
+ // // If the depth is greater than 2 you can lift
+ // return block.depth > 2;
+ // })
+ true
+ }
+ mainTooltip="Decrease Indent"
+ secondaryTooltip={formatKeyboardShortcut("Shift+Tab")}
+ icon={RiIndentDecrease}
+ />
+
+
+ {/* {
+ const comment = this.props.commentStore.createComment();
+ props.editor.chain().focus().setComment(comment.id).run();
+ }}
+ styleDetails={comment}
+ /> */}
+
+ );
+};
diff --git a/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx b/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx
new file mode 100644
index 0000000000..3ca28f6a21
--- /dev/null
+++ b/packages/react/src/FormattingToolbar/components/LinkToolbarButton.tsx
@@ -0,0 +1,49 @@
+import Tippy from "@tippyjs/react";
+import { useCallback, useState } from "react";
+import {
+ ToolbarButton,
+ ToolbarButtonProps,
+} from "../../SharedComponents/Toolbar/components/ToolbarButton";
+import { EditHyperlinkMenu } from "../../HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu";
+
+type HyperlinkButtonProps = ToolbarButtonProps & {
+ hyperlinkIsActive: boolean;
+ activeHyperlinkUrl: string;
+ activeHyperlinkText: string;
+ setHyperlink: (url: string, text?: string) => void;
+};
+
+/**
+ * The link menu button opens a tooltip on click
+ */
+export const LinkToolbarButton = (props: HyperlinkButtonProps) => {
+ const [creationMenu, setCreationMenu] = useState();
+
+ // TODO: review code; does this pattern still make sense?
+ const updateCreationMenu = useCallback(() => {
+ setCreationMenu(
+
+ );
+ }, [props]);
+
+ return (
+ {
+ updateCreationMenu();
+ }}
+ interactive={true}
+ maxWidth={500}>
+
+
+ );
+};
+
+export default LinkToolbarButton;
diff --git a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx b/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx
similarity index 99%
rename from packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx
rename to packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx
index aa931c57e5..cd5898dc8a 100644
--- a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx
+++ b/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx
@@ -14,12 +14,13 @@ export type EditHyperlinkMenuProps = {
* Provides input fields for setting the hyperlink URL and title.
*/
export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => {
- const [url, setUrl] = useState(props.url);
- const [title, setTitle] = useState(props.text);
const { classes } = createStyles({ root: {} })(undefined, {
name: "EditHyperlinkMenu",
});
+ const [url, setUrl] = useState(props.url);
+ const [title, setTitle] = useState(props.text);
+
return (
(null);
-
- useEffect(() => {
- setTimeout(() => {
- props.autofocus && inputRef.current?.focus();
- });
- }, [props.autofocus]);
-
function handleEnter(event: KeyboardEvent) {
if (event.key === "Enter") {
event.preventDefault();
@@ -29,12 +21,12 @@ export function EditHyperlinkMenuItemInput(
return (
props.onChange(event.currentTarget.value)}
onKeyDown={handleEnter}
placeholder={props.placeholder}
- ref={inputRef}
/>
);
}
diff --git a/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx b/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx
new file mode 100644
index 0000000000..c8f6c1e6ec
--- /dev/null
+++ b/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx
@@ -0,0 +1,61 @@
+import { createRoot, Root } from "react-dom/client";
+import {
+ HyperlinkToolbar,
+ HyperlinkToolbarDynamicParams,
+ HyperlinkToolbarFactory,
+ HyperlinkToolbarStaticParams,
+} from "@blocknote/core";
+import { MantineProvider } from "@mantine/core";
+import Tippy from "@tippyjs/react";
+import { HyperlinkToolbar as ReactHyperlinkToolbar } from "./components/HyperlinkToolbar";
+import { BlockNoteTheme } from "../BlockNoteTheme";
+// import rootStyles from "../../../core/src/root.module.css";
+
+export const ReactHyperlinkToolbarFactory: HyperlinkToolbarFactory = (
+ staticParams: HyperlinkToolbarStaticParams
+): HyperlinkToolbar => {
+ // We don't use the document body as a root as it would cause multiple React roots to be created on a single element
+ // if other UI factories do the same.
+ const rootElement = document.createElement("div");
+ let root: Root | undefined;
+
+ function getComponent(dynamicParams: HyperlinkToolbarDynamicParams) {
+ return (
+
+
+ }
+ duration={0}
+ getReferenceClientRect={() => dynamicParams.boundingBox}
+ hideOnClick={false}
+ interactive={true}
+ placement={"top"}
+ showOnCreate={true}
+ trigger={"manual"}
+ />
+
+ );
+ }
+
+ return {
+ element: rootElement,
+ render: (
+ dynamicParams: HyperlinkToolbarDynamicParams,
+ isHidden: boolean
+ ) => {
+ if (isHidden) {
+ document.body.appendChild(rootElement);
+ root = createRoot(rootElement);
+ }
+
+ root!.render(getComponent(dynamicParams));
+ },
+ hide: () => {
+ root!.unmount();
+
+ rootElement.remove();
+ },
+ };
+};
diff --git a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbar.tsx b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbar.tsx
new file mode 100644
index 0000000000..f2ce16b66b
--- /dev/null
+++ b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbar.tsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+import { EditHyperlinkMenu } from "../EditHyperlinkMenu/components/EditHyperlinkMenu";
+import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar";
+import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton";
+import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri";
+// import rootStyles from "../../../root.module.css";
+
+export type HyperlinkToolbarProps = {
+ url: string;
+ text: string;
+ editHyperlink: (url: string, text: string) => void;
+ deleteHyperlink: () => void;
+};
+
+/**
+ * Main menu component for the hyperlink extension.
+ * Renders a toolbar that appears on hyperlink hover.
+ */
+export const HyperlinkToolbar = (props: HyperlinkToolbarProps) => {
+ const [isEditing, setIsEditing] = useState(false);
+
+ if (isEditing) {
+ return (
+
+ );
+ }
+
+ return (
+
+ setIsEditing(true)}>
+ Edit Link
+
+ {
+ window.open(props.url, "_blank");
+ }}
+ icon={RiExternalLinkFill}
+ />
+
+
+ );
+};
diff --git a/packages/core/src/shared/components/toolbar/Toolbar.tsx b/packages/react/src/SharedComponents/Toolbar/components/Toolbar.tsx
similarity index 100%
rename from packages/core/src/shared/components/toolbar/Toolbar.tsx
rename to packages/react/src/SharedComponents/Toolbar/components/Toolbar.tsx
diff --git a/packages/core/src/shared/components/toolbar/ToolbarButton.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx
similarity index 90%
rename from packages/core/src/shared/components/toolbar/ToolbarButton.tsx
rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx
index fe457e8253..61767afe40 100644
--- a/packages/core/src/shared/components/toolbar/ToolbarButton.tsx
+++ b/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx
@@ -1,8 +1,7 @@
import { ActionIcon, Button } from "@mantine/core";
import Tippy from "@tippyjs/react";
import { forwardRef } from "react";
-import { TooltipContent } from "../tooltip/TooltipContent";
-import React from "react";
+import { TooltipContent } from "../../Tooltip/components/TooltipContent";
import { IconType } from "react-icons";
export type ToolbarButtonProps = {
@@ -16,7 +15,7 @@ export type ToolbarButtonProps = {
};
/**
- * Helper for basic buttons that show in the inline bubble menu.
+ * Helper for basic buttons that show in the formatting toolbar.
*/
export const ToolbarButton = forwardRef((props: ToolbarButtonProps, ref) => {
const ButtonIcon = props.icon;
diff --git a/packages/core/src/shared/components/toolbar/ToolbarDropdown.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx
similarity index 100%
rename from packages/core/src/shared/components/toolbar/ToolbarDropdown.tsx
rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx
diff --git a/packages/core/src/shared/components/toolbar/ToolbarDropdownItem.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx
similarity index 100%
rename from packages/core/src/shared/components/toolbar/ToolbarDropdownItem.tsx
rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx
diff --git a/packages/core/src/shared/components/toolbar/ToolbarDropdownTarget.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownTarget.tsx
similarity index 100%
rename from packages/core/src/shared/components/toolbar/ToolbarDropdownTarget.tsx
rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownTarget.tsx
diff --git a/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx
new file mode 100644
index 0000000000..ddd5782822
--- /dev/null
+++ b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx
@@ -0,0 +1,19 @@
+import { createStyles, Stack, Text } from "@mantine/core";
+
+export const TooltipContent = (props: {
+ mainTooltip: string;
+ secondaryTooltip?: string;
+}) => {
+ const { classes } = createStyles({ root: {} })(undefined, {
+ name: "Tooltip",
+ });
+
+ return (
+
+ {props.mainTooltip}
+ {props.secondaryTooltip && (
+ {props.secondaryTooltip}
+ )}
+
+ );
+};
diff --git a/packages/react/src/SlashMenu/SlashMenuFactory.tsx b/packages/react/src/SlashMenu/SlashMenuFactory.tsx
new file mode 100644
index 0000000000..add31771ec
--- /dev/null
+++ b/packages/react/src/SlashMenu/SlashMenuFactory.tsx
@@ -0,0 +1,63 @@
+import { createRoot, Root } from "react-dom/client";
+import {
+ SlashMenuItem,
+ SuggestionsMenu,
+ SuggestionsMenuDynamicParams,
+ SuggestionsMenuFactory,
+ SuggestionsMenuStaticParams,
+} from "@blocknote/core";
+import { MantineProvider } from "@mantine/core";
+import Tippy from "@tippyjs/react";
+import { SlashMenu } from "./components/SlashMenu";
+import { BlockNoteTheme } from "../BlockNoteTheme";
+// import rootStyles from "../../../core/src/root.module.css";
+
+export const ReactSlashMenuFactory: SuggestionsMenuFactory = (
+ staticParams: SuggestionsMenuStaticParams
+): SuggestionsMenu => {
+ // We don't use the document body as a root as it would cause multiple React roots to be created on a single element
+ // if other menu factories do the same.
+ const rootElement = document.createElement("div");
+ // rootElement.className = rootStyles.bnRoot;
+ let root: Root | undefined;
+
+ function getComponent(
+ dynamicParams: SuggestionsMenuDynamicParams
+ ) {
+ return (
+
+ }
+ duration={0}
+ getReferenceClientRect={() => dynamicParams.queryStartBoundingBox}
+ hideOnClick={false}
+ interactive={true}
+ placement={"bottom-start"}
+ showOnCreate={true}
+ trigger={"manual"}
+ />
+
+ );
+ }
+
+ return {
+ element: rootElement as HTMLElement,
+ render: (
+ dynamicParams: SuggestionsMenuDynamicParams,
+ isHidden: boolean
+ ) => {
+ if (isHidden) {
+ document.body.appendChild(rootElement);
+ root = createRoot(rootElement);
+ }
+
+ root!.render(getComponent(dynamicParams));
+ },
+ hide: () => {
+ root!.unmount();
+
+ rootElement.remove();
+ },
+ };
+};
diff --git a/packages/react/src/SlashMenu/components/SlashMenu.tsx b/packages/react/src/SlashMenu/components/SlashMenu.tsx
new file mode 100644
index 0000000000..9aa81f1058
--- /dev/null
+++ b/packages/react/src/SlashMenu/components/SlashMenu.tsx
@@ -0,0 +1,108 @@
+import { SlashMenuItem } from "@blocknote/core";
+import { createStyles, Menu } from "@mantine/core";
+import { SlashMenuItem as ReactSlashMenuItem } from "./SlashMenuItem";
+
+export type SlashMenuProps = {
+ items: SlashMenuItem[];
+ selectedItemIndex: number;
+ itemCallback: (item: SlashMenuItem) => void;
+};
+
+export function SlashMenu(props: SlashMenuProps) {
+ const { classes } = createStyles({ root: {} })(undefined, {
+ name: "SlashMenu",
+ });
+
+ const headingGroup: SlashMenuItem[] = [];
+ const basicBlockGroup: SlashMenuItem[] = [];
+
+ for (const item of props.items) {
+ if (item.name === "Heading") {
+ headingGroup.push(item);
+ }
+
+ if (item.name === "Heading 2") {
+ headingGroup.push(item);
+ }
+
+ if (item.name === "Heading 3") {
+ headingGroup.push(item);
+ }
+
+ if (item.name === "Numbered List") {
+ basicBlockGroup.push(item);
+ }
+
+ if (item.name === "Bullet List") {
+ basicBlockGroup.push(item);
+ }
+
+ if (item.name === "Paragraph") {
+ basicBlockGroup.push(item);
+ }
+ }
+
+ const renderedItems: any[] = [];
+ let index = 0;
+
+ if (headingGroup.length > 0) {
+ renderedItems.push(
+ {"Headings"}
+ );
+
+ for (const item of headingGroup) {
+ renderedItems.push(
+ props.itemCallback(item)}
+ />
+ );
+ index++;
+ }
+ }
+
+ if (basicBlockGroup.length > 0) {
+ renderedItems.push(
+ {"Basic Blocks"}
+ );
+
+ for (const item of basicBlockGroup) {
+ renderedItems.push(
+ props.itemCallback(item)}
+ />
+ );
+ index++;
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx b/packages/react/src/SlashMenu/components/SlashMenuItem.tsx
similarity index 62%
rename from packages/core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx
rename to packages/react/src/SlashMenu/components/SlashMenuItem.tsx
index 11967985a0..5fbc5aeb8f 100644
--- a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx
+++ b/packages/react/src/SlashMenu/components/SlashMenuItem.tsx
@@ -1,27 +1,35 @@
-import SuggestionItem from "../SuggestionItem";
-import { useEffect, useRef } from "react";
import { Badge, createStyles, Menu, Stack, Text } from "@mantine/core";
+import { useEffect, useRef } from "react";
+import { IconType } from "react-icons";
+
+import {
+ RiH1,
+ RiH2,
+ RiH3,
+ RiListOrdered,
+ RiListUnordered,
+ RiText,
+} from "react-icons/ri";
const MIN_LEFT_MARGIN = 5;
-export type SuggestionGroupItemProps = {
- item: T;
- index: number;
- selectedIndex?: number;
- clickItem: (item: T) => void;
+export type SlashMenuItemProps = {
+ name: string;
+ hint: string | undefined;
+ shortcut?: string;
+ isSelected: boolean;
+ set: () => void;
};
-export function SuggestionGroupItem(
- props: SuggestionGroupItemProps
-) {
+export function SlashMenuItem(props: SlashMenuItemProps) {
const itemRef = useRef(null);
const { classes } = createStyles({ root: {} })(undefined, {
name: "SuggestionListItem",
});
function isSelected() {
- const isKeyboardSelected =
- props.selectedIndex !== undefined && props.selectedIndex === props.index;
+ const isKeyboardSelected = props.isSelected;
+ // props.selectedIndex !== undefined && props.selectedIndex === props.index;
const isMouseSelected = itemRef.current?.matches(":hover");
return isKeyboardSelected || isMouseSelected;
@@ -53,29 +61,54 @@ export function SuggestionGroupItem(
}
});
- const Icon = props.item.icon;
+ // TODO: rearchitect, this is hacky
+ let Icon: IconType | undefined;
+ switch (props.name) {
+ case "Heading":
+ Icon = RiH1;
+ break;
+ case "Heading 2":
+ Icon = RiH2;
+ break;
+
+ case "Heading 3":
+ Icon = RiH3;
+ break;
+ case "Numbered List":
+ Icon = RiListOrdered;
+ break;
+ case "Bullet List":
+ Icon = RiListUnordered;
+ break;
+ case "Paragraph":
+ Icon = RiText;
+ break;
+ default:
+ break;
+ }
return (
}
- onClick={() => props.clickItem(props.item)}
+ onClick={props.set}
+ closeMenuOnClick={false}
// Ensures an item selected with both mouse & keyboard doesn't get deselected on mouse leave.
onMouseLeave={() => {
setTimeout(() => {
updateSelection();
- });
+ }, 1);
}}
ref={itemRef}
rightSection={
- props.item.shortcut && {props.item.shortcut}
+ props.shortcut && {props.shortcut}
}>
{/*Might need separate classes.*/}
- {props.item.name}
+ {props.name}
- {props.item.hint}
+ {props.hint}
);
diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts
new file mode 100644
index 0000000000..e907de768d
--- /dev/null
+++ b/packages/react/src/hooks/useBlockNote.ts
@@ -0,0 +1,63 @@
+import { BlockNoteEditor, BlockNoteEditorOptions } from "@blocknote/core";
+import { DependencyList, useEffect, useState } from "react";
+import { ReactFormattingToolbarFactory } from "../FormattingToolbar/FormattingToolbarFactory";
+import { ReactHyperlinkToolbarFactory } from "../HyperlinkToolbar/HyperlinkToolbarFactory";
+import { ReactSlashMenuFactory } from "../SlashMenu/SlashMenuFactory";
+import { ReactBlockSideMenuFactory } from "../BlockSideMenu/BlockSideMenuFactory";
+
+//based on https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts
+
+function useForceUpdate() {
+ const [, setValue] = useState(0);
+
+ return () => setValue((value) => value + 1);
+}
+
+/**
+ * Main hook for importing a BlockNote editor into a React project
+ */
+export const useBlockNote = (
+ options: Partial = {},
+ deps: DependencyList = []
+) => {
+ const [editor, setEditor] = useState(null);
+ const forceUpdate = useForceUpdate();
+
+ useEffect(() => {
+ let isMounted = true;
+ let newOptions = { ...options };
+ if (!newOptions.uiFactories) {
+ newOptions = {
+ ...newOptions,
+ uiFactories: {
+ formattingToolbarFactory: ReactFormattingToolbarFactory,
+ hyperlinkToolbarFactory: ReactHyperlinkToolbarFactory,
+ slashMenuFactory: ReactSlashMenuFactory,
+ blockSideMenuFactory: ReactBlockSideMenuFactory,
+ },
+ };
+ }
+ console.log("create new blocknote instance");
+ const instance = new BlockNoteEditor(newOptions);
+
+ setEditor(instance);
+
+ instance.tiptapEditor.on("transaction", () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (isMounted) {
+ forceUpdate();
+ }
+ });
+ });
+ });
+
+ return () => {
+ instance.tiptapEditor.destroy();
+ isMounted = false;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, deps);
+
+ return editor;
+};
diff --git a/packages/core/src/shared/hooks/useEditorForceUpdate.tsx b/packages/react/src/hooks/useEditorForceUpdate.tsx
similarity index 100%
rename from packages/core/src/shared/hooks/useEditorForceUpdate.tsx
rename to packages/react/src/hooks/useEditorForceUpdate.tsx
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
new file mode 100644
index 0000000000..01fcd48d2b
--- /dev/null
+++ b/packages/react/src/index.ts
@@ -0,0 +1,8 @@
+// TODO: review directories
+export * from "./BlockNoteView";
+export * from "./BlockSideMenu/BlockSideMenuFactory";
+export * from "./FormattingToolbar/FormattingToolbarFactory";
+export * from "./hooks/useBlockNote";
+export * from "./hooks/useEditorForceUpdate";
+export * from "./HyperlinkToolbar/HyperlinkToolbarFactory";
+export * from "./SlashMenu/SlashMenuFactory";
diff --git a/packages/react/src/tsconfig.json b/packages/react/src/tsconfig.json
new file mode 100644
index 0000000000..8841d64b1c
--- /dev/null
+++ b/packages/react/src/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "jsx": "react-jsx",
+ "strict": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "types",
+ "composite": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/react/src/types/styles.d.ts b/packages/react/src/types/styles.d.ts
new file mode 100644
index 0000000000..f57bdaee63
--- /dev/null
+++ b/packages/react/src/types/styles.d.ts
@@ -0,0 +1 @@
+declare module "*.module.css";
\ No newline at end of file
diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts
new file mode 100644
index 0000000000..a035199358
--- /dev/null
+++ b/packages/react/src/utils.ts
@@ -0,0 +1,12 @@
+export const isAppleOS = () =>
+ /Mac/.test(navigator.platform) ||
+ (/AppleWebKit/.test(navigator.userAgent) &&
+ /Mobile\/\w+/.test(navigator.userAgent));
+
+export function formatKeyboardShortcut(shortcut: string) {
+ if (isAppleOS()) {
+ return shortcut.replace("Mod", "⌘");
+ } else {
+ return shortcut.replace("Mod", "Ctrl");
+ }
+}
diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json
new file mode 100644
index 0000000000..8841d64b1c
--- /dev/null
+++ b/packages/react/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "jsx": "react-jsx",
+ "strict": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "types",
+ "composite": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts
new file mode 100644
index 0000000000..4d66118a1e
--- /dev/null
+++ b/packages/react/vite.config.ts
@@ -0,0 +1,35 @@
+import react from "@vitejs/plugin-react";
+import * as path from "path";
+import { defineConfig } from "vite";
+import pkg from "./package.json";
+// import eslintPlugin from "vite-plugin-eslint";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ sourcemap: true,
+ lib: {
+ entry: path.resolve(__dirname, "src/index.ts"),
+ name: "blocknote-react",
+ fileName: "blocknote-react",
+ },
+ rollupOptions: {
+ // make sure to externalize deps that shouldn't be bundled
+ // into your library
+ external: Object.keys({
+ ...pkg.dependencies,
+ ...pkg.peerDependencies,
+ ...pkg.devDependencies,
+ }),
+ output: {
+ // Provide global variables to use in the UMD build
+ // for externalized deps
+ globals: {
+ react: "React",
+ "react-dom": "ReactDOM",
+ },
+ },
+ },
+ },
+});
diff --git a/tests/end-to-end/dragdrop/dragdrop.test.ts b/tests/end-to-end/dragdrop/dragdrop.test.ts
index ac671cb08f..9a1c5f9b1a 100644
--- a/tests/end-to-end/dragdrop/dragdrop.test.ts
+++ b/tests/end-to-end/dragdrop/dragdrop.test.ts
@@ -9,6 +9,7 @@ import {
import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor";
import { insertHeading, insertParagraph } from "../../utils/copypaste";
import { dragAndDropBlock } from "../../utils/mouse";
+import { showMouseCursor } from "../../utils/debug";
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL, { waitUntil: "networkidle" });
@@ -24,6 +25,7 @@ test.describe("Check Block Dragging Functionality", () => {
"Playwright doesn't correctly simulate drag events in Firefox."
);
await focusOnEditor(page);
+ await showMouseCursor(page);
await insertHeading(page, 1);
await insertHeading(page, 2);
@@ -31,6 +33,7 @@ test.describe("Check Block Dragging Functionality", () => {
const dragTarget = await page.locator(H_ONE_BLOCK_SELECTOR);
const dropTarget = await page.locator(H_TWO_BLOCK_SELECTOR);
+ await page.pause();
await dragAndDropBlock(page, dragTarget, dropTarget, false);
await page.pause();
@@ -47,6 +50,7 @@ test.describe("Check Block Dragging Functionality", () => {
"Playwright doesn't correctly simulate drag events in Firefox."
);
await focusOnEditor(page);
+ await showMouseCursor(page);
await insertHeading(page, 1);
await insertParagraph(page);
@@ -63,16 +67,19 @@ test.describe("Check Block Dragging Functionality", () => {
// Dragging first heading into next nested element.
let dragTarget = await page.locator(H_ONE_BLOCK_SELECTOR);
let dropTarget = await page.locator(H_TWO_BLOCK_SELECTOR);
+ await page.pause();
await dragAndDropBlock(page, dragTarget, dropTarget, true);
// Dragging second heading into next nested element.
dragTarget = await page.locator(H_TWO_BLOCK_SELECTOR);
dropTarget = await page.locator(H_THREE_BLOCK_SELECTOR);
+ await page.pause();
await dragAndDropBlock(page, dragTarget, dropTarget, true);
// Dragging third heading into outside nesting.
dragTarget = await page.locator(H_THREE_BLOCK_SELECTOR);
dropTarget = await page.locator(BLOCK_SELECTOR).last();
+ await page.pause();
await dragAndDropBlock(page, dragTarget, dropTarget, true);
await page.pause();
diff --git a/tests/end-to-end/draghandle/draghandle.test.ts b/tests/end-to-end/draghandle/draghandle.test.ts
index f9c924ea35..9c26505dfa 100644
--- a/tests/end-to-end/draghandle/draghandle.test.ts
+++ b/tests/end-to-end/draghandle/draghandle.test.ts
@@ -42,6 +42,7 @@ test.describe("Check Draghandle functionality", () => {
await page.keyboard.type("Hover over this text");
const heading = await page.locator(H_ONE_BLOCK_SELECTOR);
await moveMouseOverElement(page, heading);
+
await page.waitForSelector(DRAG_HANDLE_SELECTOR);
});
@@ -117,6 +118,7 @@ test.describe("Check Draghandle functionality", () => {
const heading = await page.locator(H_ONE_BLOCK_SELECTOR);
await moveMouseOverElement(page, heading);
await page.click(DRAG_HANDLE_ADD_SELECTOR);
+
await page.waitForSelector(SLASH_MENU_SELECTOR);
});
@@ -125,6 +127,7 @@ test.describe("Check Draghandle functionality", () => {
await page.keyboard.type("Hover over this text");
await hoverAndAddBlockFromDragHandle(page, H_ONE_BLOCK_SELECTOR, "h2");
await page.waitForSelector(DRAG_HANDLE_SELECTOR, { state: "detached" });
+
await page.waitForSelector(DRAG_HANDLE_ADD_SELECTOR, { state: "detached" });
});
@@ -156,7 +159,6 @@ test.describe("Check Draghandle functionality", () => {
await page.waitForSelector(H_TWO_BLOCK_SELECTOR, { state: "detached" });
await page.waitForSelector(H_THREE_BLOCK_SELECTOR);
- // Compare doc object snapshot
await compareDocToSnapshot(page, "dragHandleDocStructure");
});
diff --git a/tests/end-to-end/draghandle/draghandle.test.ts-snapshots/draghandlemenu-firefox-linux.png b/tests/end-to-end/draghandle/draghandle.test.ts-snapshots/draghandlemenu-firefox-linux.png
index 3960ede100..33cc6b28d4 100644
Binary files a/tests/end-to-end/draghandle/draghandle.test.ts-snapshots/draghandlemenu-firefox-linux.png and b/tests/end-to-end/draghandle/draghandle.test.ts-snapshots/draghandlemenu-firefox-linux.png differ
diff --git a/tests/utils/const.ts b/tests/utils/const.ts
index 2a8e271e56..0ab5e9ba0d 100644
--- a/tests/utils/const.ts
+++ b/tests/utils/const.ts
@@ -18,6 +18,6 @@ export const DRAG_HANDLE_SELECTOR = `[data-test="dragHandle"]`;
export const DRAG_HANDLE_ADD_SELECTOR = `[data-test="dragHandleAdd"]`;
export const DRAG_HANDLE_MENU_SELECTOR = `.mantine-DragHandleMenu-root`;
-export const SLASH_MENU_SELECTOR = `.mantine-SuggestionList-root`;
+export const SLASH_MENU_SELECTOR = `.mantine-SlashMenu-root`;
export const TYPE_DELAY = 10;
diff --git a/tests/utils/mouse.ts b/tests/utils/mouse.ts
index ba324446bc..308b4f39b7 100644
--- a/tests/utils/mouse.ts
+++ b/tests/utils/mouse.ts
@@ -15,6 +15,14 @@ async function getElementRightCoords(page: Page, element: Locator) {
return { x: boundingBox.x + boundingBox.width - 1, y: centerY };
}
+async function getElementCenterCoords(page: Page, element: Locator) {
+ const boundingBox = await element.boundingBox();
+ const centerX = boundingBox.x + boundingBox.width / 2;
+ const centerY = boundingBox.y + boundingBox.height / 2;
+
+ return { x: centerX, y: centerY };
+}
+
export async function moveMouseOverElement(page: Page, element: Locator) {
const boundingBox = await element.boundingBox();
const coords = { x: boundingBox.x, y: boundingBox.y };
@@ -31,7 +39,10 @@ export async function dragAndDropBlock(
await page.waitForSelector(DRAG_HANDLE_SELECTOR);
const dragHandle = await page.locator(DRAG_HANDLE_SELECTOR);
- await moveMouseOverElement(page, dragHandle);
+ const dragHandleCenterCoords = await getElementCenterCoords(page, dragHandle);
+ await page.mouse.move(dragHandleCenterCoords.x, dragHandleCenterCoords.y, {
+ steps: 5,
+ });
await page.mouse.down();
const dropTargetCoords = dropAbove