diff --git a/docs/components/pages/landing/community/Sponsors.tsx b/docs/components/pages/landing/community/Sponsors.tsx
index c9684e28b9..1625aaac1c 100644
--- a/docs/components/pages/landing/community/Sponsors.tsx
+++ b/docs/components/pages/landing/community/Sponsors.tsx
@@ -5,29 +5,59 @@ import {
import { FadeIn } from "@/components/pages/landing/shared/FadeIn";
import { SectionSubHeader } from "@/components/pages/landing/shared/Headings";
-import deeporigin from "../../../../public/img/sponsors/deeporigin.svg";
-import fermatDark from "../../../../public/img/sponsors/fermat-dark.svg";
-import fermatLight from "../../../../public/img/sponsors/fermat.svg";
-import nlnetDark from "../../../../public/img/sponsors/nlnet-dark.svg";
-import nlnetLight from "../../../../public/img/sponsors/nlnet.svg";
-import noteplanDark from "../../../../public/img/sponsors/noteplan-dark.png";
-import noteplanLight from "../../../../public/img/sponsors/noteplan.png";
-import poggioDark from "../../../../public/img/sponsors/poggio-dark.svg";
-import poggioLight from "../../../../public/img/sponsors/poggio.svg";
-import twentyDark from "../../../../public/img/sponsors/twenty-dark.png";
-import twentyLight from "../../../../public/img/sponsors/twenty.png";
-import typecellDark from "../../../../public/img/sponsors/typecell-dark.svg";
-import typecellLight from "../../../../public/img/sponsors/typecell.svg";
+import atuin from "../../../../public/img/sponsors/atuin.png";
+import capitolDark from "../../../../public/img/sponsors/capitolDark.svg";
+import capitolLight from "../../../../public/img/sponsors/capitolLight.svg";
+import deepOrigin from "../../../../public/img/sponsors/deepOrigin.svg";
+import dinumDark from "../../../../public/img/sponsors/dinumDark.svg";
+import dinumLight from "../../../../public/img/sponsors/dinumLight.svg";
+import fermatDark from "../../../../public/img/sponsors/fermatDark.svg";
+import fermatLight from "../../../../public/img/sponsors/fermatLight.svg";
+import nlnetDark from "../../../../public/img/sponsors/nlnetDark.svg";
+import nlnetLight from "../../../../public/img/sponsors/nlnetLight.svg";
+import notePlanDark from "../../../../public/img/sponsors/notePlanDark.png";
+import notePlanLight from "../../../../public/img/sponsors/notePlanLight.png";
+import poggioDark from "../../../../public/img/sponsors/poggioDark.svg";
+import poggioLight from "../../../../public/img/sponsors/poggioLight.svg";
+import twentyDark from "../../../../public/img/sponsors/twentyDark.png";
+import twentyLight from "../../../../public/img/sponsors/twentyLight.png";
+import typeCellDark from "../../../../public/img/sponsors/typeCellDark.svg";
+import typeCellLight from "../../../../public/img/sponsors/typeCellLight.svg";
+import zendis from "../../../../public/img/sponsors/zendis.svg";
export const sponsorsCardData: SponsorCardProps[] = [
{
logo: {
- light: deeporigin,
- dark: deeporigin,
+ light: atuin,
+ dark: atuin,
+ },
+ name: "Atuin",
+ link: "https://atuin.sh/",
+ },
+ {
+ logo: {
+ light: capitolLight,
+ dark: capitolDark,
+ },
+ name: "Capitol",
+ link: "https://www.capitol.ai/",
+ },
+ {
+ logo: {
+ light: deepOrigin,
+ dark: deepOrigin,
},
name: "Deep Origin",
link: "https://www.deeporigin.com/",
},
+ {
+ logo: {
+ light: dinumLight,
+ dark: dinumDark,
+ },
+ name: "DINUM",
+ link: "https://www.numerique.gouv.fr/dinum/",
+ },
{
logo: {
light: fermatLight,
@@ -46,10 +76,10 @@ export const sponsorsCardData: SponsorCardProps[] = [
},
{
logo: {
- light: noteplanLight,
- dark: noteplanDark,
+ light: notePlanLight,
+ dark: notePlanDark,
},
- name: "Noteplan",
+ name: "NotePlan",
link: "https://noteplan.co/",
tagline: "Apple Top Notes Apps",
},
@@ -72,12 +102,20 @@ export const sponsorsCardData: SponsorCardProps[] = [
},
{
logo: {
- light: typecellLight,
- dark: typecellDark,
+ light: typeCellLight,
+ dark: typeCellDark,
},
name: "TypeCell",
link: "https://www.typecell.org/",
},
+ {
+ logo: {
+ light: zendis,
+ dark: zendis,
+ },
+ name: "ZenDiS",
+ link: "https://zendis.de/",
+ },
];
export function Sponsors() {
diff --git a/docs/next.config.mjs b/docs/next.config.mjs
index e3de90a927..092a11b5f0 100644
--- a/docs/next.config.mjs
+++ b/docs/next.config.mjs
@@ -118,6 +118,11 @@ const nextConfig = withAnalyzer(
destination: "/docs/advanced/vanilla-js",
permanent: true,
},
+ {
+ source: "/examples/basic/all-blocks",
+ destination: "/examples/basic/default-blocks",
+ permanent: true,
+ },
],
experimental: {
externalDir: true,
diff --git a/docs/package.json b/docs/package.json
index f0d51b45a1..aa31da5938 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,6 +1,6 @@
{
"name": "docs",
- "version": "0.22.0",
+ "version": "0.23.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,12 +9,12 @@
"lint": "next lint"
},
"dependencies": {
- "@blocknote/ariakit": "^0.22.0",
- "@blocknote/core": "^0.22.0",
- "@blocknote/mantine": "^0.22.0",
- "@blocknote/react": "^0.22.0",
- "@blocknote/shadcn": "^0.22.0",
- "@blocknote/xl-multi-column": "^0.22.0",
+ "@blocknote/ariakit": "^0.23.1",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/mantine": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
+ "@blocknote/shadcn": "^0.23.1",
+ "@blocknote/xl-multi-column": "^0.23.1",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.4",
"@mantine/core": "^7.10.1",
diff --git a/docs/pages/docs/advanced/real-time-collaboration.mdx b/docs/pages/docs/advanced/real-time-collaboration.mdx
index 2a9b5dc6cd..7d8059800a 100644
--- a/docs/pages/docs/advanced/real-time-collaboration.mdx
+++ b/docs/pages/docs/advanced/real-time-collaboration.mdx
@@ -38,6 +38,9 @@ const editor = useCreateBlockNote({
name: "My Username",
color: "#ff0000",
},
+ // When to show user labels on the collaboration cursor. Set by default to
+ // "activity" (show when the cursor moves), but can also be set to "always".
+ showCursorLabels: "activity"
},
// ...
});
diff --git a/docs/pages/docs/editor-basics/default-schema.mdx b/docs/pages/docs/editor-basics/default-schema.mdx
index 9787910c1a..cd1c09966e 100644
--- a/docs/pages/docs/editor-basics/default-schema.mdx
+++ b/docs/pages/docs/editor-basics/default-schema.mdx
@@ -14,7 +14,7 @@ BlockNote supports a number of built-in blocks, inline content types, and styles
The demo below showcases each of BlockNote's built-in block and inline content types:
-
+
## Default Blocks
diff --git a/docs/public/img/sponsors/atuin.png b/docs/public/img/sponsors/atuin.png
new file mode 100644
index 0000000000..6a4c5ac3de
Binary files /dev/null and b/docs/public/img/sponsors/atuin.png differ
diff --git a/docs/public/img/sponsors/capitolDark.svg b/docs/public/img/sponsors/capitolDark.svg
new file mode 100644
index 0000000000..357d0e20bf
--- /dev/null
+++ b/docs/public/img/sponsors/capitolDark.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/img/sponsors/capitolLight.svg b/docs/public/img/sponsors/capitolLight.svg
new file mode 100644
index 0000000000..3e7b6c1de2
--- /dev/null
+++ b/docs/public/img/sponsors/capitolLight.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/public/img/sponsors/deeporigin.svg b/docs/public/img/sponsors/deepOrigin.svg
similarity index 100%
rename from docs/public/img/sponsors/deeporigin.svg
rename to docs/public/img/sponsors/deepOrigin.svg
diff --git a/docs/public/img/sponsors/dinumDark.svg b/docs/public/img/sponsors/dinumDark.svg
new file mode 100644
index 0000000000..633c37d8b0
--- /dev/null
+++ b/docs/public/img/sponsors/dinumDark.svg
@@ -0,0 +1,181 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/img/sponsors/dinumLight.svg b/docs/public/img/sponsors/dinumLight.svg
new file mode 100644
index 0000000000..d6f9a3d91c
--- /dev/null
+++ b/docs/public/img/sponsors/dinumLight.svg
@@ -0,0 +1,159 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/img/sponsors/fermat-dark.svg b/docs/public/img/sponsors/fermatDark.svg
similarity index 100%
rename from docs/public/img/sponsors/fermat-dark.svg
rename to docs/public/img/sponsors/fermatDark.svg
diff --git a/docs/public/img/sponsors/fermat.svg b/docs/public/img/sponsors/fermatLight.svg
similarity index 100%
rename from docs/public/img/sponsors/fermat.svg
rename to docs/public/img/sponsors/fermatLight.svg
diff --git a/docs/public/img/sponsors/nlnet-dark.svg b/docs/public/img/sponsors/nlnetDark.svg
similarity index 100%
rename from docs/public/img/sponsors/nlnet-dark.svg
rename to docs/public/img/sponsors/nlnetDark.svg
diff --git a/docs/public/img/sponsors/nlnet.svg b/docs/public/img/sponsors/nlnetLight.svg
similarity index 100%
rename from docs/public/img/sponsors/nlnet.svg
rename to docs/public/img/sponsors/nlnetLight.svg
diff --git a/docs/public/img/sponsors/noteplan-dark.png b/docs/public/img/sponsors/notePlanDark.png
similarity index 100%
rename from docs/public/img/sponsors/noteplan-dark.png
rename to docs/public/img/sponsors/notePlanDark.png
diff --git a/docs/public/img/sponsors/noteplan.png b/docs/public/img/sponsors/notePlanLight.png
similarity index 100%
rename from docs/public/img/sponsors/noteplan.png
rename to docs/public/img/sponsors/notePlanLight.png
diff --git a/docs/public/img/sponsors/poggio-dark.svg b/docs/public/img/sponsors/poggioDark.svg
similarity index 100%
rename from docs/public/img/sponsors/poggio-dark.svg
rename to docs/public/img/sponsors/poggioDark.svg
diff --git a/docs/public/img/sponsors/poggio.svg b/docs/public/img/sponsors/poggioLight.svg
similarity index 100%
rename from docs/public/img/sponsors/poggio.svg
rename to docs/public/img/sponsors/poggioLight.svg
diff --git a/docs/public/img/sponsors/twenty-dark.png b/docs/public/img/sponsors/twentyDark.png
similarity index 100%
rename from docs/public/img/sponsors/twenty-dark.png
rename to docs/public/img/sponsors/twentyDark.png
diff --git a/docs/public/img/sponsors/twenty.png b/docs/public/img/sponsors/twentyLight.png
similarity index 100%
rename from docs/public/img/sponsors/twenty.png
rename to docs/public/img/sponsors/twentyLight.png
diff --git a/docs/public/img/sponsors/typecell-dark.svg b/docs/public/img/sponsors/typeCellDark.svg
similarity index 100%
rename from docs/public/img/sponsors/typecell-dark.svg
rename to docs/public/img/sponsors/typeCellDark.svg
diff --git a/docs/public/img/sponsors/typecell.svg b/docs/public/img/sponsors/typeCellLight.svg
similarity index 100%
rename from docs/public/img/sponsors/typecell.svg
rename to docs/public/img/sponsors/typeCellLight.svg
diff --git a/docs/public/img/sponsors/zendis.svg b/docs/public/img/sponsors/zendis.svg
new file mode 100644
index 0000000000..7cdafa9098
--- /dev/null
+++ b/docs/public/img/sponsors/zendis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx
index a3b92bafd2..d4fd6f2e12 100644
--- a/examples/01-basic/01-minimal/App.tsx
+++ b/examples/01-basic/01-minimal/App.tsx
@@ -5,7 +5,7 @@ import { useCreateBlockNote } from "@blocknote/react";
export default function App() {
// Creates a new editor instance.
- const editor = useCreateBlockNote();
+ const editor = useCreateBlockNote({});
// Renders the editor instance using a React component.
return ;
diff --git a/examples/01-basic/04-all-blocks/.bnexample.json b/examples/01-basic/04-all-blocks/.bnexample.json
deleted file mode 100644
index d38bcda2ee..0000000000
--- a/examples/01-basic/04-all-blocks/.bnexample.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "playground": true,
- "docs": true,
- "author": "yousefed",
- "tags": ["Basic", "Blocks", "Inline Content"],
- "dependencies": {
- "@blocknote/xl-multi-column": "latest"
- }
-}
diff --git a/examples/01-basic/04-default-blocks/.bnexample.json b/examples/01-basic/04-default-blocks/.bnexample.json
new file mode 100644
index 0000000000..6b15063ef2
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["Basic", "Blocks", "Inline Content"]
+}
diff --git a/examples/01-basic/04-all-blocks/App.tsx b/examples/01-basic/04-default-blocks/App.tsx
similarity index 67%
rename from examples/01-basic/04-all-blocks/App.tsx
rename to examples/01-basic/04-default-blocks/App.tsx
index 73681ebcf0..935e96d93e 100644
--- a/examples/01-basic/04-all-blocks/App.tsx
+++ b/examples/01-basic/04-default-blocks/App.tsx
@@ -1,33 +1,11 @@
-import {
- BlockNoteSchema,
- combineByGroup,
- filterSuggestionItems,
- locales,
-} from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
-import {
- SuggestionMenuController,
- getDefaultReactSlashMenuItems,
- useCreateBlockNote,
-} from "@blocknote/react";
-import {
- getMultiColumnSlashMenuItems,
- multiColumnDropCursor,
- locales as multiColumnLocales,
- withMultiColumn,
-} from "@blocknote/xl-multi-column";
-import { useMemo } from "react";
+import { useCreateBlockNote } from "@blocknote/react";
+
export default function App() {
// Creates a new editor instance.
const editor = useCreateBlockNote({
- schema: withMultiColumn(BlockNoteSchema.create()),
- dropCursor: multiColumnDropCursor,
- dictionary: {
- ...locales.en,
- multi_column: multiColumnLocales.en,
- },
initialContent: [
{
type: "paragraph",
@@ -50,35 +28,6 @@ export default function App() {
type: "paragraph",
content: "Paragraph",
},
- {
- type: "columnList",
- children: [
- {
- type: "column",
- props: {
- width: 0.8,
- },
- children: [
- {
- type: "paragraph",
- content: "Hello to the left!",
- },
- ],
- },
- {
- type: "column",
- props: {
- width: 1.2,
- },
- children: [
- {
- type: "paragraph",
- content: "Hello to the right!",
- },
- ],
- },
- ],
- },
{
type: "heading",
content: "Heading",
@@ -188,20 +137,6 @@ export default function App() {
],
});
- const slashMenuItems = useMemo(() => {
- return combineByGroup(
- getDefaultReactSlashMenuItems(editor),
- getMultiColumnSlashMenuItems(editor)
- );
- }, [editor]);
-
// Renders the editor instance using a React component.
- return (
-
- filterSuggestionItems(slashMenuItems, query)}
- />
-
- );
+ return ;
}
diff --git a/examples/01-basic/04-all-blocks/README.md b/examples/01-basic/04-default-blocks/README.md
similarity index 100%
rename from examples/01-basic/04-all-blocks/README.md
rename to examples/01-basic/04-default-blocks/README.md
diff --git a/examples/01-basic/04-all-blocks/index.html b/examples/01-basic/04-default-blocks/index.html
similarity index 100%
rename from examples/01-basic/04-all-blocks/index.html
rename to examples/01-basic/04-default-blocks/index.html
diff --git a/examples/01-basic/04-all-blocks/main.tsx b/examples/01-basic/04-default-blocks/main.tsx
similarity index 100%
rename from examples/01-basic/04-all-blocks/main.tsx
rename to examples/01-basic/04-default-blocks/main.tsx
diff --git a/examples/01-basic/04-all-blocks/package.json b/examples/01-basic/04-default-blocks/package.json
similarity index 87%
rename from examples/01-basic/04-all-blocks/package.json
rename to examples/01-basic/04-default-blocks/package.json
index 352d433f1f..d59814a01a 100644
--- a/examples/01-basic/04-all-blocks/package.json
+++ b/examples/01-basic/04-default-blocks/package.json
@@ -1,5 +1,5 @@
{
- "name": "@blocknote/example-all-blocks",
+ "name": "@blocknote/example-default-blocks",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"private": true,
"version": "0.12.4",
@@ -17,8 +17,7 @@
"@blocknote/mantine": "latest",
"@blocknote/shadcn": "latest",
"react": "^18.3.1",
- "react-dom": "^18.3.1",
- "@blocknote/xl-multi-column": "latest"
+ "react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.0.25",
diff --git a/examples/01-basic/04-all-blocks/tsconfig.json b/examples/01-basic/04-default-blocks/tsconfig.json
similarity index 100%
rename from examples/01-basic/04-all-blocks/tsconfig.json
rename to examples/01-basic/04-default-blocks/tsconfig.json
diff --git a/examples/01-basic/04-all-blocks/vite.config.ts b/examples/01-basic/04-default-blocks/vite.config.ts
similarity index 100%
rename from examples/01-basic/04-all-blocks/vite.config.ts
rename to examples/01-basic/04-default-blocks/vite.config.ts
diff --git a/examples/01-basic/12-multi-editor/.bnexample.json b/examples/01-basic/12-multi-editor/.bnexample.json
new file mode 100644
index 0000000000..54cfd20571
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "areknawo",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/12-multi-editor/App.tsx b/examples/01-basic/12-multi-editor/App.tsx
new file mode 100644
index 0000000000..edc2a84c0e
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/App.tsx
@@ -0,0 +1,55 @@
+import { PartialBlock } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+// Component that creates & renders a BlockNote editor.
+function Editor(props: { initialContent?: PartialBlock[] }) {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ sideMenuDetection: "editor",
+ initialContent: props.initialContent,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
+
+export default function App() {
+ // Creates & renders two editors side by side.
+ return (
+
-
+
+
+ filterSuggestionItems(slashMenuItems, query)
+ }
+ />
+
{pdfDocument}
diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx b/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx
index 3f0618546e..94bb8d8c91 100644
--- a/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx
+++ b/examples/05-interoperability/06-converting-blocks-to-docx/App.tsx
@@ -1,16 +1,30 @@
+import {
+ BlockNoteSchema,
+ combineByGroup,
+ filterSuggestionItems,
+ withPageBreak,
+} from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
-import { useCreateBlockNote } from "@blocknote/react";
+import {
+ getDefaultReactSlashMenuItems,
+ getPageBreakReactSlashMenuItems,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
import {
DOCXExporter,
docxDefaultSchemaMappings,
} from "@blocknote/xl-docx-exporter";
+import { useMemo } from "react";
+
import "./styles.css";
export default function App() {
// Creates a new editor instance with some initial content.
const editor = useCreateBlockNote({
+ schema: withPageBreak(BlockNoteSchema.create()),
initialContent: [
{
type: "paragraph",
@@ -175,6 +189,9 @@ export default function App() {
],
},
},
+ {
+ type: "pageBreak",
+ },
{
type: "file",
},
@@ -277,6 +294,15 @@ export default function App() {
],
},
},
+ {
+ type: "codeBlock",
+ props: {
+ language: "javascript",
+ },
+ content: `const helloWorld = (message) => {
+ console.log("Hello World", message);
+};`,
+ },
],
});
@@ -296,6 +322,13 @@ export default function App() {
window.URL.revokeObjectURL(link.href);
};
+ const slashMenuItems = useMemo(() => {
+ return combineByGroup(
+ getDefaultReactSlashMenuItems(editor),
+ getPageBreakReactSlashMenuItems(editor)
+ );
+ }, [editor]);
+
// Renders the editor instance, and its contents as HTML below.
return (
@@ -305,7 +338,14 @@ export default function App() {
-
+
+
+ filterSuggestionItems(slashMenuItems, query)
+ }
+ />
+
);
diff --git a/examples/07-collaboration/02-liveblocks/tsconfig.json b/examples/07-collaboration/02-liveblocks/tsconfig.json
index 1bd8ab3c57..4a76cf4c7d 100644
--- a/examples/07-collaboration/02-liveblocks/tsconfig.json
+++ b/examples/07-collaboration/02-liveblocks/tsconfig.json
@@ -3,11 +3,7 @@
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
- "lib": [
- "DOM",
- "DOM.Iterable",
- "ESNext"
- ],
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
@@ -22,10 +18,8 @@
"jsx": "react-jsx",
"composite": true
},
- "include": [
- "."
- ],
- "__ADD_FOR_LOCAL_DEV_references": [
+ "include": ["."],
+ "references": [
{
"path": "../../../packages/core/"
},
@@ -33,4 +27,4 @@
"path": "../../../packages/react/"
}
]
-}
\ No newline at end of file
+}
diff --git a/lerna.json b/lerna.json
index e5d282f591..43751f86a7 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,5 +2,5 @@
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useNx": false,
"useWorkspaces": true,
- "version": "0.22.0"
+ "version": "0.23.1"
}
diff --git a/package-lock.json b/package-lock.json
index a1ed9a79e0..566a3b1140 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,15 +24,155 @@
"typescript": "^5.3.3"
}
},
+ "../liveblocks/packages/liveblocks-client": {
+ "name": "@liveblocks/client",
+ "version": "2.15.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@liveblocks/core": "2.15.2"
+ },
+ "devDependencies": {
+ "@liveblocks/eslint-config": "*",
+ "@liveblocks/jest-config": "*",
+ "@types/ws": "^8.5.10",
+ "dotenv": "^16.4.5",
+ "msw": "^0.39.1",
+ "ws": "^8.17.1"
+ }
+ },
+ "../liveblocks/packages/liveblocks-react": {
+ "name": "@liveblocks/react",
+ "version": "2.15.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@liveblocks/client": "2.15.2",
+ "@liveblocks/core": "2.15.2"
+ },
+ "devDependencies": {
+ "@liveblocks/eslint-config": "*",
+ "@liveblocks/jest-config": "*",
+ "@liveblocks/query-parser": "^0.0.4",
+ "@testing-library/jest-dom": "6.4.6",
+ "@testing-library/react": "14.1.2",
+ "date-fns": "^3.6.0",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "itertools": "^2.3.2",
+ "msw": "1.3.2",
+ "react-error-boundary": "^4.0.13"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "../liveblocks/packages/liveblocks-react-blocknote": {
+ "version": "2.15.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@liveblocks/client": "2.15.2",
+ "@liveblocks/core": "2.15.2",
+ "@liveblocks/react": "2.15.2",
+ "@liveblocks/react-tiptap": "2.15.2",
+ "@liveblocks/react-ui": "2.15.2",
+ "@liveblocks/yjs": "2.15.2",
+ "@tiptap/core": "^2.7.2"
+ },
+ "devDependencies": {
+ "@liveblocks/eslint-config": "*",
+ "@liveblocks/jest-config": "*",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-replace": "^5.0.5",
+ "@rollup/plugin-typescript": "^11.1.2",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@types/use-sync-external-store": "^0.0.6",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "msw": "^0.27.1",
+ "rollup": "^3.28.0",
+ "rollup-plugin-dts": "^5.3.1",
+ "rollup-plugin-esbuild": "^5.0.0",
+ "rollup-plugin-preserve-directives": "^0.2.0",
+ "stylelint": "^15.10.2",
+ "stylelint-config-standard": "^34.0.0",
+ "stylelint-order": "^6.0.3",
+ "stylelint-plugin-logical-css": "^0.13.2"
+ },
+ "peerDependencies": {
+ "@blocknote/core": "^0.19.1",
+ "@blocknote/react": "^0.19.1",
+ "@tiptap/core": "^2.7.2",
+ "react": "^16.14.0 || ^17 || ^18",
+ "react-dom": "^16.14.0 || ^17 || ^18"
+ }
+ },
+ "../liveblocks/packages/liveblocks-react-ui": {
+ "name": "@liveblocks/react-ui",
+ "version": "2.15.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.2",
+ "@liveblocks/client": "2.15.2",
+ "@liveblocks/core": "2.15.2",
+ "@liveblocks/react": "2.15.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
+ "@radix-ui/react-popover": "^1.1.2",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-toggle": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.3",
+ "react-virtuoso": "^4.12.0",
+ "slate": "^0.110.2",
+ "slate-history": "^0.110.3",
+ "slate-hyperscript": "^0.100.0",
+ "slate-react": "^0.110.3"
+ },
+ "devDependencies": {
+ "@liveblocks/eslint-config": "*",
+ "@liveblocks/jest-config": "*",
+ "@liveblocks/rollup-config": "*",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.1.1",
+ "emojibase": "^15.3.0",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "msw": "^0.27.1",
+ "rollup": "3.28.0",
+ "stylelint": "^15.10.2",
+ "stylelint-config-standard": "^34.0.0",
+ "stylelint-order": "^6.0.3",
+ "stylelint-plugin-logical-css": "^0.13.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "../liveblocks/packages/liveblocks-yjs": {
+ "name": "@liveblocks/yjs",
+ "version": "2.15.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@liveblocks/client": "2.15.2",
+ "@liveblocks/core": "2.15.2",
+ "js-base64": "^3.7.7",
+ "y-indexeddb": "^9.0.12"
+ },
+ "devDependencies": {
+ "@liveblocks/eslint-config": "*",
+ "@liveblocks/jest-config": "*",
+ "@testing-library/jest-dom": "^6.4.6",
+ "msw": "^0.47.4"
+ },
+ "peerDependencies": {
+ "yjs": "^13.6.1"
+ }
+ },
"docs": {
- "version": "0.22.0",
- "dependencies": {
- "@blocknote/ariakit": "^0.22.0",
- "@blocknote/core": "^0.22.0",
- "@blocknote/mantine": "^0.22.0",
- "@blocknote/react": "^0.22.0",
- "@blocknote/shadcn": "^0.22.0",
- "@blocknote/xl-multi-column": "^0.22.0",
+ "version": "0.23.1",
+ "dependencies": {
+ "@blocknote/ariakit": "^0.23.1",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/mantine": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
+ "@blocknote/shadcn": "^0.23.1",
+ "@blocknote/xl-multi-column": "^0.23.1",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.4",
"@mantine/core": "^7.10.1",
@@ -3322,6 +3462,13 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@base2/pretty-print-object": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz",
+ "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/@blocknote/ariakit": {
"resolved": "packages/ariakit",
"link": true
@@ -3501,6 +3648,15 @@
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="
},
+ "node_modules/@emoji-mart/react": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz",
+ "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==",
+ "peerDependencies": {
+ "emoji-mart": "^5.2",
+ "react": "^16.8 || ^17 || ^18"
+ }
+ },
"node_modules/@emotion/babel-plugin": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz",
@@ -4703,11 +4859,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@juggle/resize-observer": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
- "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
- },
"node_modules/@lerna/add": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/@lerna/add/-/add-5.6.2.tgz",
@@ -5773,99 +5924,24 @@
}
},
"node_modules/@liveblocks/client": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-2.11.0.tgz",
- "integrity": "sha512-ZRayUwwOzucYn4QqOTz0vcQSZlqGaP8TBROzE9Ye/PwfACNasyfa3roZqfizhBlYlYQQ/8qQlvGl2n3LPf8Byg==",
- "dependencies": {
- "@liveblocks/core": "2.11.0"
- }
- },
- "node_modules/@liveblocks/core": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-2.11.0.tgz",
- "integrity": "sha512-2WQlJvJ1NVJ/CpKhDNJJHXrh2Yzz6eXchqn64JqTHk5TLGbx1s4kaTahEXaAOXmu2abnJWnv06v1mhv3YXYg+A=="
+ "resolved": "../liveblocks/packages/liveblocks-client",
+ "link": true
},
"node_modules/@liveblocks/react": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-2.11.0.tgz",
- "integrity": "sha512-uAPEh9ZS03ANTnbbU5PTiFVusI05H7EqOH4aDVaKDrogkKi70RYbCek4PKY8TpK2vLhmutBxyVd3tp2uBwvTFQ==",
- "dependencies": {
- "@liveblocks/client": "2.11.0",
- "@liveblocks/core": "2.11.0",
- "use-sync-external-store": "^1.2.2"
- },
- "peerDependencies": {
- "react": "^16.14.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
- }
- },
- "node_modules/@liveblocks/react-ui": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/@liveblocks/react-ui/-/react-ui-2.11.0.tgz",
- "integrity": "sha512-aBrgLWclSoV+3tOXxf45pL6Uaf8mRvHTmda79kwyCB/8yjqoeqNe2PThcELL8WWyaZsl7XU6Bgjpat3zY5rRHQ==",
- "dependencies": {
- "@floating-ui/react-dom": "^2.1.2",
- "@liveblocks/client": "2.11.0",
- "@liveblocks/core": "2.11.0",
- "@liveblocks/react": "2.11.0",
- "@radix-ui/react-dropdown-menu": "^2.1.2",
- "@radix-ui/react-popover": "^1.1.2",
- "@radix-ui/react-slot": "^1.1.0",
- "@radix-ui/react-toggle": "^1.1.0",
- "@radix-ui/react-tooltip": "^1.1.3",
- "react-virtuoso": "^4.12.0",
- "slate": "^0.110.2",
- "slate-history": "^0.110.3",
- "slate-hyperscript": "^0.100.0",
- "slate-react": "^0.110.3",
- "use-sync-external-store": "^1.2.2"
- },
- "peerDependencies": {
- "react": "^16.14.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
- }
+ "resolved": "../liveblocks/packages/liveblocks-react",
+ "link": true
},
- "node_modules/@liveblocks/react-ui/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
- "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
+ "node_modules/@liveblocks/react-blocknote": {
+ "resolved": "../liveblocks/packages/liveblocks-react-blocknote",
+ "link": true
},
- "node_modules/@liveblocks/react-ui/node_modules/@radix-ui/react-slot": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
- "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
+ "node_modules/@liveblocks/react-ui": {
+ "resolved": "../liveblocks/packages/liveblocks-react-ui",
+ "link": true
},
"node_modules/@liveblocks/yjs": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/@liveblocks/yjs/-/yjs-2.11.0.tgz",
- "integrity": "sha512-o9RfjrVUMW4htHvHGwXxptZxnjC+evEvEkBqS2uPhOBBtOSMe15rWqzhoHqRXngkMUs8BIZ0EOkM5a0qsFIF0w==",
- "dependencies": {
- "@liveblocks/client": "2.11.0",
- "@liveblocks/core": "2.11.0",
- "js-base64": "^3.7.7"
- },
- "peerDependencies": {
- "yjs": "^13.6.1"
- }
+ "resolved": "../liveblocks/packages/liveblocks-yjs",
+ "link": true
},
"node_modules/@mantine/core": {
"version": "7.10.1",
@@ -15685,18 +15761,6 @@
"node": ">=8"
}
},
- "node_modules/direction": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz",
- "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==",
- "bin": {
- "direction": "cli.js"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -19186,15 +19250,6 @@
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
- "node_modules/immer": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
- "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/immer"
- }
- },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -19724,11 +19779,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/is-hotkey": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
- "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
- },
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -20291,11 +20341,6 @@
"url": "https://github.com/sponsors/panva"
}
},
- "node_modules/js-base64": {
- "version": "3.7.7",
- "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
- "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -26428,6 +26473,29 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-element-to-jsx-string": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz",
+ "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@base2/pretty-print-object": "1.0.1",
+ "is-plain-object": "5.0.0",
+ "react-is": "18.1.0"
+ },
+ "peerDependencies": {
+ "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0",
+ "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0"
+ }
+ },
+ "node_modules/react-element-to-jsx-string/node_modules/react-is": {
+ "version": "18.1.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz",
+ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/react-github-btn": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz",
@@ -26616,18 +26684,6 @@
"react-dom": ">=16.6.0"
}
},
- "node_modules/react-virtuoso": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.0.tgz",
- "integrity": "sha512-oHrKlU7xHsrnBQ89ecZoMPAK0tHnI9s1hsFW3KKg5ZGeZ5SWvbGhg/QFJFY4XETAzoCUeu+Xaxn1OUb/PGtPlA==",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "react": ">=16 || >=17 || >= 18",
- "react-dom": ">=16 || >=17 || >= 18"
- }
- },
"node_modules/read": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
@@ -28270,57 +28326,6 @@
"node": ">=8"
}
},
- "node_modules/slate": {
- "version": "0.110.2",
- "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz",
- "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==",
- "dependencies": {
- "immer": "^10.0.3",
- "is-plain-object": "^5.0.0",
- "tiny-warning": "^1.0.3"
- }
- },
- "node_modules/slate-history": {
- "version": "0.110.3",
- "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.110.3.tgz",
- "integrity": "sha512-sgdff4Usdflmw5ZUbhDkxFwCBQ2qlDKMMkF93w66KdV48vHOgN2BmLrf+2H8SdX8PYIpP/cTB0w8qWC2GwhDVA==",
- "dependencies": {
- "is-plain-object": "^5.0.0"
- },
- "peerDependencies": {
- "slate": ">=0.65.3"
- }
- },
- "node_modules/slate-hyperscript": {
- "version": "0.100.0",
- "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz",
- "integrity": "sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==",
- "dependencies": {
- "is-plain-object": "^5.0.0"
- },
- "peerDependencies": {
- "slate": ">=0.65.3"
- }
- },
- "node_modules/slate-react": {
- "version": "0.110.3",
- "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.3.tgz",
- "integrity": "sha512-AS8PPjwmsFS3Lq0MOEegLVlFoxhyos68G6zz2nW4sh3WeTXV7pX0exnwtY1a/docn+J3LGQO11aZXTenPXA/kg==",
- "dependencies": {
- "@juggle/resize-observer": "^3.4.0",
- "direction": "^1.0.4",
- "is-hotkey": "^0.2.0",
- "is-plain-object": "^5.0.0",
- "lodash": "^4.17.21",
- "scroll-into-view-if-needed": "^3.1.0",
- "tiny-invariant": "1.3.1"
- },
- "peerDependencies": {
- "react": ">=18.2.0",
- "react-dom": ">=18.2.0",
- "slate": ">=0.99.0"
- }
- },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -29172,16 +29177,6 @@
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
},
- "node_modules/tiny-invariant": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
- "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
- },
- "node_modules/tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
- },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -31651,12 +31646,12 @@
},
"packages/ariakit": {
"name": "@blocknote/ariakit",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "MPL-2.0",
"dependencies": {
"@ariakit/react": "^0.4.3",
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0"
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1"
},
"devDependencies": {
"@types/react": "^18.0.25",
@@ -31695,7 +31690,7 @@
},
"packages/core": {
"name": "@blocknote/core",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "MPL-2.0",
"dependencies": {
"@emoji-mart/data": "^1.2.1",
@@ -31852,7 +31847,7 @@
},
"packages/dev-scripts": {
"name": "@blocknote/dev-scripts",
- "version": "0.22.0",
+ "version": "0.23.0",
"license": "MPL-2.0",
"devDependencies": {
"@types/react": "^18.0.25",
@@ -31869,11 +31864,11 @@
},
"packages/mantine": {
"name": "@blocknote/mantine",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "MPL-2.0",
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@mantine/core": "^7.10.1",
"@mantine/hooks": "^7.10.1",
"@mantine/utils": "^6.0.21",
@@ -31917,10 +31912,12 @@
},
"packages/react": {
"name": "@blocknote/react",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "MPL-2.0",
"dependencies": {
- "@blocknote/core": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@emoji-mart/data": "^1.2.1",
+ "@emoji-mart/react": "^1.1.1",
"@floating-ui/react": "^0.26.4",
"@tiptap/core": "^2.7.1",
"@tiptap/react": "^2.7.1",
@@ -31968,11 +31965,11 @@
},
"packages/server-util": {
"name": "@blocknote/server-util",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "MPL-2.0",
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@tiptap/core": "^2.7.1",
"@tiptap/pm": "^2.7.1",
"jsdom": "^25.0.1",
@@ -31999,11 +31996,11 @@
},
"packages/shadcn": {
"name": "@blocknote/shadcn",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "MPL-2.0",
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@hookform/resolvers": "^3.6.0",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
@@ -32063,10 +32060,10 @@
},
"packages/xl-docx-exporter": {
"name": "@blocknote/xl-docx-exporter",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "AGPL-3.0 OR PROPRIETARY",
"dependencies": {
- "@blocknote/core": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
"buffer": "^6.0.3",
"docx": "^9.0.2",
"sharp": "^0.33.5"
@@ -32082,6 +32079,10 @@
"vite-plugin-eslint": "^1.8.1",
"vitest": "^2.0.3",
"xml-formatter": "^3.6.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || >= 19.0.0-rc",
+ "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc"
}
},
"packages/xl-docx-exporter/node_modules/buffer": {
@@ -32109,11 +32110,11 @@
},
"packages/xl-multi-column": {
"name": "@blocknote/xl-multi-column",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "AGPL-3.0 OR PROPRIETARY",
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@tiptap/core": "^2.7.1",
"prosemirror-model": "^1.23.0",
"prosemirror-state": "^1.4.3",
@@ -32133,6 +32134,10 @@
"vite": "^5.3.4",
"vite-plugin-eslint": "^1.8.1",
"vitest": "^2.0.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || >= 19.0.0-rc",
+ "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc"
}
},
"packages/xl-multi-column/node_modules/cssstyle": {
@@ -32344,11 +32349,11 @@
},
"packages/xl-pdf-exporter": {
"name": "@blocknote/xl-pdf-exporter",
- "version": "0.22.0",
+ "version": "0.23.1",
"license": "AGPL-3.0 OR PROPRIETARY",
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@react-pdf/renderer": "^4.0.0",
"buffer": "^6.0.3",
"docx": "^9.0.2"
@@ -32363,6 +32368,7 @@
"jest-image-snapshot": "^6.4.0",
"pdf-to-img": "^4.2.0",
"prettier": "^2.7.1",
+ "react-element-to-jsx-string": "^15.0.0",
"rollup-plugin-webpack-stats": "^0.2.2",
"typescript": "^5.0.4",
"vite": "^5.3.4",
@@ -32431,23 +32437,24 @@
},
"playground": {
"name": "@blocknote/example-editor",
- "version": "0.22.0",
+ "version": "0.23.1",
"dependencies": {
"@aws-sdk/client-s3": "^3.609.0",
"@aws-sdk/s3-request-presigner": "^3.609.0",
- "@blocknote/ariakit": "^0.22.0",
- "@blocknote/core": "^0.22.0",
- "@blocknote/mantine": "^0.22.0",
- "@blocknote/react": "^0.22.0",
- "@blocknote/server-util": "^0.22.0",
- "@blocknote/shadcn": "^0.22.0",
- "@blocknote/xl-multi-column": "^0.22.0",
+ "@blocknote/ariakit": "^0.23.1",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/mantine": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
+ "@blocknote/server-util": "^0.23.1",
+ "@blocknote/shadcn": "^0.23.1",
+ "@blocknote/xl-multi-column": "^0.23.1",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
- "@liveblocks/client": "^2.11.0",
- "@liveblocks/react": "^2.11.0",
- "@liveblocks/react-ui": "^2.11.0",
- "@liveblocks/yjs": "^2.11.0",
+ "@liveblocks/client": "file:../../liveblocks/packages/liveblocks-client",
+ "@liveblocks/react": "file:../../liveblocks/packages/liveblocks-react",
+ "@liveblocks/react-blocknote": "file:../../liveblocks/packages/liveblocks-react-blocknote",
+ "@liveblocks/react-ui": "file:../../liveblocks/packages/liveblocks-react-ui",
+ "@liveblocks/yjs": "file:../../liveblocks/packages/liveblocks-yjs",
"@mantine/core": "^7.10.1",
"@mui/icons-material": "^5.16.1",
"@mui/material": "^5.16.1",
@@ -32504,9 +32511,9 @@
},
"shared": {
"name": "@blocknote/shared",
- "version": "0.22.0",
+ "version": "0.23.1",
"dependencies": {
- "@blocknote/core": "^0.22.0"
+ "@blocknote/core": "^0.23.1"
},
"devDependencies": {
"typescript": "^5.3.3"
@@ -32514,17 +32521,17 @@
},
"tests": {
"name": "@blocknote/tests",
- "version": "0.22.0",
+ "version": "0.23.1",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
- "@blocknote/ariakit": "^0.22.0",
- "@blocknote/core": "^0.22.0",
- "@blocknote/mantine": "^0.22.0",
- "@blocknote/react": "^0.22.0",
- "@blocknote/shadcn": "^0.22.0",
+ "@blocknote/ariakit": "^0.23.1",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/mantine": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
+ "@blocknote/shadcn": "^0.23.1",
"@playwright/experimental-ct-react": "^1.49.1",
"@playwright/test": "^1.49.1",
"eslint": "^8.10.0",
diff --git a/packages/ariakit/package.json b/packages/ariakit/package.json
index ef43192759..d289e74e75 100644
--- a/packages/ariakit/package.json
+++ b/packages/ariakit/package.json
@@ -6,7 +6,7 @@
"*.css"
],
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -51,8 +51,8 @@
},
"dependencies": {
"@ariakit/react": "^0.4.3",
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0"
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1"
},
"devDependencies": {
"@types/react": "^18.0.25",
diff --git a/packages/core/package.json b/packages/core/package.json
index 0ba63d920b..d4118c75f6 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -6,7 +6,7 @@
"*.css"
],
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
diff --git a/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html
new file mode 100644
index 0000000000..8c7757e46a
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html
@@ -0,0 +1 @@
+
Paragraph
Heading Numbered List Item
console.log("Hello World");
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Add image
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html
new file mode 100644
index 0000000000..4397413824
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html
@@ -0,0 +1 @@
+
Paragraph
Heading Numbered List Item
console.log("Hello World");
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Table Cell
Placeholder
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/clipboardInternal.test.ts b/packages/core/src/api/clipboard/clipboardInternal.test.ts
index 29b5393f07..0fb136575e 100644
--- a/packages/core/src/api/clipboard/clipboardInternal.test.ts
+++ b/packages/core/src/api/clipboard/clipboardInternal.test.ts
@@ -148,6 +148,122 @@ describe("Test ProseMirror selection clipboard HTML", () => {
type: "customParagraph",
content: "Paragraph",
},
+ {
+ type: "paragraph",
+ content: "Paragraph",
+ },
+ {
+ type: "heading",
+ content: "Heading",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item",
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item",
+ },
+ {
+ type: "codeBlock",
+ content: 'console.log("Hello World");',
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]],
+ },
+ {
+ cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]],
+ },
+ {
+ cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]],
+ },
+ ],
+ },
+ },
+ {
+ type: "image",
+ },
+ {
+ type: "paragraph",
+ props: {
+ textColor: "red",
+ },
+ content: "Paragraph",
+ },
+ {
+ type: "heading",
+ props: {
+ level: 2,
+ },
+ content: "Heading",
+ },
+ {
+ type: "numberedListItem",
+ props: {
+ start: 2,
+ },
+ content: "Numbered List Item",
+ },
+ {
+ type: "bulletListItem",
+ props: {
+ backgroundColor: "red",
+ },
+ content: "Bullet List Item",
+ },
+ {
+ type: "checkListItem",
+ props: {
+ checked: true,
+ },
+ content: "Check List Item",
+ },
+ {
+ type: "codeBlock",
+ props: {
+ language: "typescript",
+ },
+ content: 'console.log("Hello World");',
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]],
+ },
+ {
+ cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]],
+ },
+ {
+ cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]],
+ },
+ ],
+ },
+ },
+ {
+ type: "image",
+ props: {
+ name: "1280px-Placeholder_view_vector.svg.png",
+ url: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/1280px-Placeholder_view_vector.svg.png",
+ caption: "Placeholder",
+ showPreview: true,
+ previewWidth: 256,
+ },
+ },
+ {
+ type: "paragraph",
+ },
];
let editor: BlockNoteEditor
;
@@ -299,6 +415,16 @@ describe("Test ProseMirror selection clipboard HTML", () => {
createCopySelection: (doc) => TextSelection.create(doc, 277, 286),
createPasteSelection: (doc) => TextSelection.create(doc, 290, 299),
},
+ // Copy/paste basic blocks.
+ {
+ testName: "basicBlocks",
+ createCopySelection: (doc) => TextSelection.create(doc, 303, 558),
+ },
+ // Copy/paste basic blocks with props.
+ {
+ testName: "basicBlocksWithProps",
+ createCopySelection: (doc) => TextSelection.create(doc, 558, 813),
+ },
];
for (const testCase of testCases) {
diff --git a/packages/core/src/api/exporters/html/__snapshots__/pageBreak/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/pageBreak/basic/external.html
new file mode 100644
index 0000000000..ba65bb0620
--- /dev/null
+++ b/packages/core/src/api/exporters/html/__snapshots__/pageBreak/basic/external.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/__snapshots__/pageBreak/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/pageBreak/basic/internal.html
new file mode 100644
index 0000000000..f4303a5ad1
--- /dev/null
+++ b/packages/core/src/api/exporters/html/__snapshots__/pageBreak/basic/internal.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/markdown/__snapshots__/pageBreak/basic/markdown.md b/packages/core/src/api/exporters/markdown/__snapshots__/pageBreak/basic/markdown.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
index 4b6cebdb80..ef16974ad5 100644
--- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
+++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
@@ -1605,6 +1605,22 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert
}
`;
+exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert pageBreak/basic to/from prosemirror 1`] = `
+{
+ "attrs": {
+ "backgroundColor": "default",
+ "id": "1",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "type": "pageBreak",
+ },
+ ],
+ "type": "blockContainer",
+}
+`;
+
exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/basic to/from prosemirror 1`] = `
{
"attrs": {
diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts
index 4d73393dde..455499b01d 100644
--- a/packages/core/src/api/nodeConversions/nodeToBlock.ts
+++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts
@@ -131,7 +131,10 @@ export function contentNodeToInlineContent<
} else {
const config = styleSchema[mark.type.name];
if (!config) {
- if (mark.type.name === "liveblocksCommentMark") {
+ if (
+ mark.type.spec.group?.includes("blocknoteIgnore") ||
+ mark.type.name === "liveblocksCommentMark"
+ ) {
// TODO
continue;
}
diff --git a/packages/core/src/api/parsers/html/__snapshots__/parse-codeblocks.json b/packages/core/src/api/parsers/html/__snapshots__/parse-codeblocks.json
new file mode 100644
index 0000000000..1481967f47
--- /dev/null
+++ b/packages/core/src/api/parsers/html/__snapshots__/parse-codeblocks.json
@@ -0,0 +1,62 @@
+[
+ {
+ "id": "1",
+ "type": "codeBlock",
+ "props": {
+ "language": "javascript"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "console.log(\"Should default to JS\")",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "2",
+ "type": "codeBlock",
+ "props": {
+ "language": "typescript"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "console.log(\"Should parse TS from data-language\")",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "3",
+ "type": "codeBlock",
+ "props": {
+ "language": "python"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "print(\"Should parse Python from language- class\")",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "4",
+ "type": "codeBlock",
+ "props": {
+ "language": "typescript"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "console.log(\"Should prioritize TS from data-language over language- class\")",
+ "styles": {}
+ }
+ ],
+ "children": []
+ }
+]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts
index fe41fe41c9..2a246da242 100644
--- a/packages/core/src/api/parsers/html/parseHTML.test.ts
+++ b/packages/core/src/api/parsers/html/parseHTML.test.ts
@@ -516,4 +516,13 @@ With Hard Break
await parseHTMLAndCompareSnapshots(html, "parse-google-docs-html");
});
+
+ it("Parse codeblocks", async () => {
+ const html = `console.log("Should default to JS")
+ console.log("Should parse TS from data-language")
+ print("Should parse Python from language- class")
+ console.log("Should prioritize TS from data-language over language- class")
`;
+
+ await parseHTMLAndCompareSnapshots(html, "parse-codeblocks");
+ });
});
diff --git a/packages/core/src/api/testUtil/cases/defaultSchema.ts b/packages/core/src/api/testUtil/cases/defaultSchema.ts
index 2510f4e040..dfdff62784 100644
--- a/packages/core/src/api/testUtil/cases/defaultSchema.ts
+++ b/packages/core/src/api/testUtil/cases/defaultSchema.ts
@@ -6,16 +6,22 @@ import {
DefaultInlineContentSchema,
DefaultStyleSchema,
} from "../../../blocks/defaultBlocks.js";
+import {
+ pageBreakSchema,
+ withPageBreak,
+} from "../../../blocks/PageBreakBlockContent/schema.js";
import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import { BlockNoteSchema } from "../../../editor/BlockNoteSchema.js";
export const defaultSchemaTestCases: EditorTestCases<
- DefaultBlockSchema,
+ DefaultBlockSchema & typeof pageBreakSchema.blockSchema,
DefaultInlineContentSchema,
DefaultStyleSchema
> = {
name: "default schema",
createEditor: () => {
return BlockNoteEditor.create({
+ schema: withPageBreak(BlockNoteSchema.create()),
uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY,
});
},
@@ -202,6 +208,14 @@ export const defaultSchemaTestCases: EditorTestCases<
},
],
},
+ {
+ name: "pageBreak/basic",
+ blocks: [
+ {
+ type: "pageBreak",
+ },
+ ],
+ },
{
name: "file/button",
blocks: [
diff --git a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts
index ce71bba1bc..2143d9f686 100644
--- a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts
+++ b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts
@@ -25,6 +25,10 @@ interface CodeBlockOptions {
supportedLanguages: SupportedLanguageConfig[];
}
+export const shikiParserSymbol = Symbol.for("blocknote.shikiParser");
+export const shikiHighlighterPromiseSymbol = Symbol.for(
+ "blocknote.shikiHighlighterPromise"
+);
export const defaultCodeBlockPropSchema = {
language: {
default: "javascript",
@@ -77,9 +81,10 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
const languages = classNames
.filter((className) => className.startsWith("language-"))
.map((className) => className.replace("language-", ""));
- const [classLanguage] = languages;
- language = classLanguage.toLowerCase();
+ if (languages.length > 0) {
+ language = languages[0].toLowerCase();
+ }
}
if (!language) {
@@ -93,6 +98,7 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
);
},
renderHTML: (attributes) => {
+ // TODO: Use `data-language="..."` instead for easier parsing
return attributes.language && attributes.language !== "text"
? {
class: `language-${attributes.language}`,
@@ -106,9 +112,11 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
return [
{
tag: "div[data-content-type=" + this.name + "]",
+ contentElement: "code",
},
{
tag: "pre",
+ contentElement: "code",
preserveWhitespace: "full",
},
];
@@ -195,19 +203,30 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
};
},
addProseMirrorPlugins() {
+ const supportedLanguages = this.options
+ .supportedLanguages as SupportedLanguageConfig[];
+ const globalThisForShiki = globalThis as {
+ [shikiHighlighterPromiseSymbol]?: Promise;
+ [shikiParserSymbol]?: Parser;
+ };
+
let highlighter: Highlighter | undefined;
let parser: Parser | undefined;
- const supportedLanguages = this.options
- .supportedLanguages as SupportedLanguageConfig[];
const lazyParser: Parser = (options) => {
if (!highlighter) {
- return createHighlighter({
- themes: ["github-dark"],
- langs: [],
- }).then((createdHighlighter) => {
- highlighter = createdHighlighter;
- });
+ globalThisForShiki[shikiHighlighterPromiseSymbol] =
+ globalThisForShiki[shikiHighlighterPromiseSymbol] ||
+ createHighlighter({
+ themes: ["github-dark"],
+ langs: [],
+ });
+
+ return globalThisForShiki[shikiHighlighterPromiseSymbol].then(
+ (createdHighlighter) => {
+ highlighter = createdHighlighter;
+ }
+ );
}
const language = options.language;
@@ -223,7 +242,9 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
}
if (!parser) {
- parser = createParser(highlighter);
+ parser =
+ globalThisForShiki[shikiParserSymbol] || createParser(highlighter);
+ globalThisForShiki[shikiParserSymbol] = parser;
}
return parser(options);
diff --git a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts
index 2bf825dd6f..f20e0b4197 100644
--- a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts
+++ b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts
@@ -124,15 +124,6 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({
return [
{
tag: "div[data-content-type=" + this.name + "]",
- getAttrs: (element) => {
- if (typeof element === "string") {
- return false;
- }
-
- return {
- level: element.getAttribute("data-level"),
- };
- },
},
{
tag: "h1",
diff --git a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
index 06259794fe..e373c95242 100644
--- a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
+++ b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
@@ -79,7 +79,7 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({
return [
// Case for regular HTML list structure.
{
- tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this
+ tag: "div[data-content-type=" + this.name + "]",
},
{
tag: "li",
diff --git a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts
index 628df671a4..3c2b2a209d 100644
--- a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts
+++ b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts
@@ -118,7 +118,7 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
parseHTML() {
return [
{
- tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this
+ tag: "div[data-content-type=" + this.name + "]",
},
// Checkbox only.
{
diff --git a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
index f4de5a8ac7..d637122727 100644
--- a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
+++ b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
@@ -106,7 +106,7 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
parseHTML() {
return [
{
- tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this
+ tag: "div[data-content-type=" + this.name + "]",
},
// Case for regular HTML list structure.
// (e.g.: when pasting from other apps)
diff --git a/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts b/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts
new file mode 100644
index 0000000000..e1e72ab59c
--- /dev/null
+++ b/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts
@@ -0,0 +1,49 @@
+import {
+ createBlockSpec,
+ CustomBlockConfig,
+ Props,
+} from "../../schema/index.js";
+
+export const pageBreakConfig = {
+ type: "pageBreak" as const,
+ propSchema: {},
+ content: "none",
+ isFileBlock: false,
+ isSelectable: false,
+} satisfies CustomBlockConfig;
+export const pageBreakRender = () => {
+ const pageBreak = document.createElement("div");
+
+ pageBreak.className = "bn-page-break";
+ pageBreak.setAttribute("data-page-break", "");
+
+ return {
+ dom: pageBreak,
+ };
+};
+export const pageBreakParse = (
+ element: HTMLElement
+): Partial> | undefined => {
+ if (element.tagName === "DIV" && element.hasAttribute("data-page-break")) {
+ return {
+ type: "pageBreak",
+ };
+ }
+
+ return undefined;
+};
+export const pageBreakToExternalHTML = () => {
+ const pageBreak = document.createElement("div");
+
+ pageBreak.setAttribute("data-page-break", "");
+
+ return {
+ dom: pageBreak,
+ };
+};
+
+export const PageBreak = createBlockSpec(pageBreakConfig, {
+ render: pageBreakRender,
+ parse: pageBreakParse,
+ toExternalHTML: pageBreakToExternalHTML,
+});
diff --git a/packages/core/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.ts b/packages/core/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.ts
new file mode 100644
index 0000000000..6c7b10ad80
--- /dev/null
+++ b/packages/core/src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.ts
@@ -0,0 +1,45 @@
+import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
+import { DefaultSuggestionItem } from "../../extensions/SuggestionMenu/DefaultSuggestionItem.js";
+import { insertOrUpdateBlock } from "../../extensions/SuggestionMenu/getDefaultSlashMenuItems.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../schema/index.js";
+import { pageBreakSchema } from "./schema.js";
+
+export function checkPageBreakBlocksInSchema<
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ editor: BlockNoteEditor
+): editor is BlockNoteEditor {
+ return (
+ "pageBreak" in editor.schema.blockSchema &&
+ editor.schema.blockSchema["pageBreak"] ===
+ pageBreakSchema.blockSchema["pageBreak"]
+ );
+}
+
+export function getPageBreakSlashMenuItems<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(editor: BlockNoteEditor) {
+ const items: (Omit & { key: "page_break" })[] =
+ [];
+
+ if (checkPageBreakBlocksInSchema(editor)) {
+ items.push({
+ ...editor.dictionary.slash_menu.page_break,
+ onItemClick: () => {
+ insertOrUpdateBlock(editor, {
+ type: "pageBreak",
+ });
+ },
+ key: "page_break",
+ });
+ }
+
+ return items;
+}
diff --git a/packages/core/src/blocks/PageBreakBlockContent/schema.ts b/packages/core/src/blocks/PageBreakBlockContent/schema.ts
new file mode 100644
index 0000000000..c9b64a59c5
--- /dev/null
+++ b/packages/core/src/blocks/PageBreakBlockContent/schema.ts
@@ -0,0 +1,40 @@
+import { BlockNoteSchema } from "../../editor/BlockNoteSchema.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../schema/index.js";
+import { PageBreak } from "./PageBreakBlockContent.js";
+
+export const pageBreakSchema = BlockNoteSchema.create({
+ blockSpecs: {
+ pageBreak: PageBreak,
+ },
+});
+
+/**
+ * Adds page break support to the given schema.
+ */
+export const withPageBreak = <
+ B extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ schema: BlockNoteSchema
+) => {
+ return BlockNoteSchema.create({
+ blockSpecs: {
+ ...schema.blockSpecs,
+ ...pageBreakSchema.blockSpecs,
+ },
+ inlineContentSpecs: schema.inlineContentSpecs,
+ styleSpecs: schema.styleSpecs,
+ }) as any as BlockNoteSchema<
+ // typescript needs some help here
+ B & {
+ pageBreak: typeof PageBreak.config;
+ },
+ I,
+ S
+ >;
+};
diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css
index 54849c2b6c..6bf1061815 100644
--- a/packages/core/src/editor/Block.css
+++ b/packages/core/src/editor/Block.css
@@ -308,6 +308,20 @@ NESTED BLOCKS
transition-delay: 0.1s;
}
+/* PAGE BREAK */
+.bn-block-content[data-content-type="pageBreak"] > div {
+ width: 100%;
+ height: 0;
+ border-top: dotted rgb(125, 121, 122) 2px;
+ margin-block: 11px;
+}
+
+@media print {
+ .bn-block-content[data-content-type="pageBreak"] > div {
+ page-break-after: always;
+ }
+}
+
/* FILES */
/* Element that wraps content for all file blocks */
@@ -336,7 +350,7 @@ NESTED BLOCKS
.bn-editor[contenteditable="true"] [data-file-block] .bn-add-file-button:hover,
[data-file-block] .bn-file-name-with-icon:hover,
-.ProseMirror-selectednode .bn-file-name-with-icon{
+.ProseMirror-selectednode .bn-file-name-with-icon {
background-color: rgb(225, 225, 225);
}
@@ -523,3 +537,11 @@ NESTED BLOCKS
.bn-block-column:last-child {
padding-right: 0;
}
+
+.bn-thread-mark:not([data-orphan="true"]) {
+ background: rgba(255, 200, 0, 0.15);
+}
+
+.bn-thread-mark:not([data-orphan="true"]) .bn-thread-mark-selected {
+ background: rgba(255, 200, 0, 0.25);
+}
diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index 0231b47735..919d40a7e1 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -3,18 +3,14 @@ import {
EditorOptions,
Extension,
getSchema,
+ isNodeSelection,
Mark,
+ posToDOMRect,
Node as TipTapNode,
} from "@tiptap/core";
import { Node, Schema } from "prosemirror-model";
// import "./blocknote.css";
import * as Y from "yjs";
-import {
- getBlock,
- getNextBlock,
- getParentBlock,
- getPrevBlock,
-} from "../api/blockManipulation/getBlock/getBlock.js";
import { insertBlocks } from "../api/blockManipulation/commands/insertBlocks/insertBlocks.js";
import {
moveBlocksDown,
@@ -29,15 +25,21 @@ import {
import { removeBlocks } from "../api/blockManipulation/commands/removeBlocks/removeBlocks.js";
import { replaceBlocks } from "../api/blockManipulation/commands/replaceBlocks/replaceBlocks.js";
import { updateBlock } from "../api/blockManipulation/commands/updateBlock/updateBlock.js";
-import { insertContentAt } from "../api/blockManipulation/insertContentAt.js";
import {
- getTextCursorPosition,
- setTextCursorPosition,
-} from "../api/blockManipulation/selections/textCursorPosition/textCursorPosition.js";
+ getBlock,
+ getNextBlock,
+ getParentBlock,
+ getPrevBlock,
+} from "../api/blockManipulation/getBlock/getBlock.js";
+import { insertContentAt } from "../api/blockManipulation/insertContentAt.js";
import {
getSelection,
setSelection,
} from "../api/blockManipulation/selections/selection.js";
+import {
+ getTextCursorPosition,
+ setTextCursorPosition,
+} from "../api/blockManipulation/selections/textCursorPosition/textCursorPosition.js";
import { createExternalHTMLExporter } from "../api/exporters/html/externalHTMLExporter.js";
import { blocksToMarkdown } from "../api/exporters/markdown/markdownExporter.js";
import { HTMLToBlocks } from "../api/parsers/html/parseHTML.js";
@@ -89,11 +91,16 @@ import { en } from "../i18n/locales/index.js";
import { Plugin, Transaction } from "@tiptap/pm/state";
import { dropCursor } from "prosemirror-dropcursor";
+import { EditorView } from "prosemirror-view";
import { createInternalHTMLSerializer } from "../api/exporters/html/internalHTMLSerializer.js";
import { inlineContentToNodes } from "../api/nodeConversions/blockToNode.js";
import { nodeToBlock } from "../api/nodeConversions/nodeToBlock.js";
+import { CommentsPlugin } from "../extensions/Comments/CommentsPlugin.js";
import "../style.css";
-import { EditorView } from "prosemirror-view";
+
+export type BlockNoteExtensionFactory = (
+ editor: BlockNoteEditor
+) => BlockNoteExtension;
export type BlockNoteExtension =
| AnyExtension
@@ -196,6 +203,13 @@ export type BlockNoteEditorOptions<
* Optional function to customize how cursors of users are rendered
*/
renderCursor?: (user: any) => HTMLElement;
+ /**
+ * Optional flag to set when the user label should be shown with the default
+ * collaboration cursor. Setting to "always" will always show the label,
+ * while "activity" will only show the label when the user moves the cursor
+ * or types. Defaults to "activity".
+ */
+ showCursorLabels?: "always" | "activity";
};
/**
@@ -206,7 +220,7 @@ export type BlockNoteEditorOptions<
/**
* (experimental) add extra prosemirror plugins or tiptap extensions to the editor
*/
- _extensions: Record;
+ _extensions: Record;
trailingBlock?: boolean;
@@ -245,6 +259,15 @@ export type BlockNoteEditorOptions<
@default "prefer-navigate-ui"
*/
tabBehavior: "prefer-navigate-ui" | "prefer-indent";
+
+ /**
+ * The detection mode for showing the side menu - "viewport" always shows the
+ * side menu for the block next to the mouse cursor, while "editor" only shows
+ * it when hovering the editor or the side menu itself.
+ *
+ * @default "viewport"
+ */
+ sideMenuDetection: "viewport" | "editor";
};
const blockNoteTipTapOptions = {
@@ -329,6 +352,7 @@ export class BlockNoteEditor<
ISchema,
SSchema
>;
+ public readonly comments?: CommentsPlugin;
/**
* The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload).
@@ -423,6 +447,7 @@ export class BlockNoteEditor<
dropCursor: this.options.dropCursor ?? dropCursor,
placeholders: newOptions.placeholders,
tabBehavior: newOptions.tabBehavior,
+ sideMenuDetection: newOptions.sideMenuDetection || "viewport",
});
// add extensions from _tiptapOptions
@@ -432,6 +457,10 @@ export class BlockNoteEditor<
// add extensions from options
Object.entries(newOptions._extensions || {}).forEach(([key, ext]) => {
+ if (typeof ext === "function") {
+ // factory
+ ext = ext(this);
+ }
this.extensions[key] = ext;
});
@@ -441,6 +470,7 @@ export class BlockNoteEditor<
this.suggestionMenus = this.extensions["suggestionMenus"] as any;
this.filePanel = this.extensions["filePanel"] as any;
this.tableHandles = this.extensions["tableHandles"] as any;
+ this.comments = this.extensions["comments"] as any;
if (newOptions.uploadFile) {
const uploadFile = newOptions.uploadFile;
@@ -1206,6 +1236,28 @@ export class BlockNoteEditor<
};
}
+ public getSelectionBoundingBox() {
+ if (!this.prosemirrorView) {
+ return undefined;
+ }
+ const state = this.prosemirrorView?.state;
+ 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.prosemirrorView.nodeDOM(from) as HTMLElement;
+ if (node) {
+ return node.getBoundingClientRect();
+ }
+ }
+
+ return posToDOMRect(this.prosemirrorView, from, to);
+ }
+
public openSuggestionMenu(
triggerCharacter: string,
pluginState?: {
diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts
index b6ce68a7c7..cba56d1786 100644
--- a/packages/core/src/editor/BlockNoteExtensions.ts
+++ b/packages/core/src/editor/BlockNoteExtensions.ts
@@ -1,4 +1,5 @@
import { AnyExtension, Extension, extensions } from "@tiptap/core";
+import { Awareness } from "y-protocols/awareness";
import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
@@ -15,6 +16,8 @@ import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDrop
import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension.js";
import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
+import { CommentMark } from "../extensions/Comments/CommentMark.js";
+import { CommentsPlugin } from "../extensions/Comments/CommentsPlugin.js";
import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js";
import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js";
import { KeyboardShortcutsExtension } from "../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js";
@@ -64,6 +67,7 @@ type ExtensionOptions<
};
provider: any;
renderCursor?: (user: any) => HTMLElement;
+ showCursorLabels?: "always" | "activity";
};
disableExtensions: string[] | undefined;
setIdAttribute?: boolean;
@@ -72,6 +76,7 @@ type ExtensionOptions<
dropCursor: (opts: any) => Plugin;
placeholders: Record;
tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
+ sideMenuDetection: "viewport" | "editor";
};
/**
@@ -97,7 +102,10 @@ export const getBlockNoteExtensions = <
opts.editor
);
ret["linkToolbar"] = new LinkToolbarProsemirrorPlugin(opts.editor);
- ret["sideMenu"] = new SideMenuProsemirrorPlugin(opts.editor);
+ ret["sideMenu"] = new SideMenuProsemirrorPlugin(
+ opts.editor,
+ opts.sideMenuDetection
+ );
ret["suggestionMenus"] = new SuggestionMenuProseMirrorPlugin(opts.editor);
ret["filePanel"] = new FilePanelProsemirrorPlugin(opts.editor as any);
ret["placeholder"] = new PlaceholderPlugin(opts.editor, opts.placeholders);
@@ -120,6 +128,9 @@ export const getBlockNoteExtensions = <
ret["nodeSelectionKeyboard"] = new NodeSelectionKeyboardPlugin();
+ // TODO
+ ret["comments"] = new CommentsPlugin(opts.editor, CommentMark.name);
+
const disableExtensions: string[] = opts.disableExtensions || [];
for (const ext of disableExtensions) {
delete ret[ext];
@@ -240,6 +251,7 @@ const getTipTapExtensions = <
...(opts.trailingBlock === undefined || opts.trailingBlock
? [TrailingNode]
: []),
+ CommentMark,
];
if (opts.collaboration) {
@@ -248,25 +260,114 @@ const getTipTapExtensions = <
fragment: opts.collaboration.fragment,
})
);
- if (opts.collaboration.provider?.awareness) {
- const defaultRender = (user: { color: string; name: string }) => {
- const cursor = document.createElement("span");
- cursor.classList.add("collaboration-cursor__caret");
- cursor.setAttribute("style", `border-color: ${user.color}`);
+ const awareness = opts.collaboration?.provider.awareness as Awareness;
+
+ if (awareness) {
+ const cursors = new Map<
+ number,
+ { element: HTMLElement; hideTimeout: NodeJS.Timeout | undefined }
+ >();
+
+ if (opts.collaboration.showCursorLabels !== "always") {
+ awareness.on(
+ "change",
+ ({
+ updated,
+ }: {
+ added: Array;
+ updated: Array;
+ removed: Array;
+ }) => {
+ for (const clientID of updated) {
+ const cursor = cursors.get(clientID);
+
+ if (cursor) {
+ cursor.element.setAttribute("data-active", "");
+
+ if (cursor.hideTimeout) {
+ clearTimeout(cursor.hideTimeout);
+ }
+
+ cursors.set(clientID, {
+ element: cursor.element,
+ hideTimeout: setTimeout(() => {
+ cursor.element.removeAttribute("data-active");
+ }, 2000),
+ });
+ }
+ }
+ }
+ );
+ }
+
+ const createCursor = (clientID: number, name: string, color: string) => {
+ const cursorElement = document.createElement("span");
+
+ cursorElement.classList.add("collaboration-cursor__caret");
+ cursorElement.setAttribute("style", `border-color: ${color}`);
+ if (opts.collaboration?.showCursorLabels === "always") {
+ cursorElement.setAttribute("data-active", "");
+ }
+
+ const labelElement = document.createElement("span");
+
+ labelElement.classList.add("collaboration-cursor__label");
+ labelElement.setAttribute("style", `background-color: ${color}`);
+ labelElement.insertBefore(document.createTextNode(name), null);
+
+ cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
+ cursorElement.insertBefore(labelElement, null);
+ cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
+
+ cursors.set(clientID, {
+ element: cursorElement,
+ hideTimeout: undefined,
+ });
+
+ if (opts.collaboration?.showCursorLabels !== "always") {
+ cursorElement.addEventListener("mouseenter", () => {
+ const cursor = cursors.get(clientID)!;
+ cursor.element.setAttribute("data-active", "");
+
+ if (cursor.hideTimeout) {
+ clearTimeout(cursor.hideTimeout);
+ cursors.set(clientID, {
+ element: cursor.element,
+ hideTimeout: undefined,
+ });
+ }
+ });
+
+ cursorElement.addEventListener("mouseleave", () => {
+ const cursor = cursors.get(clientID)!;
+
+ cursors.set(clientID, {
+ element: cursor.element,
+ hideTimeout: setTimeout(() => {
+ cursor.element.removeAttribute("data-active");
+ }, 2000),
+ });
+ });
+ }
+
+ return cursors.get(clientID)!;
+ };
+
+ const defaultRender = (user: { color: string; name: string }) => {
+ const clientState = [...awareness.getStates().entries()].find(
+ (state) => state[1].user === user
+ );
- const label = document.createElement("span");
+ if (!clientState) {
+ throw new Error("Could not find client state for user");
+ }
- label.classList.add("collaboration-cursor__label");
- label.setAttribute("style", `background-color: ${user.color}`);
- label.insertBefore(document.createTextNode(user.name), null);
+ const clientID = clientState[0];
- const nonbreakingSpace1 = document.createTextNode("\u2060");
- const nonbreakingSpace2 = document.createTextNode("\u2060");
- cursor.insertBefore(nonbreakingSpace1, null);
- cursor.insertBefore(label, null);
- cursor.insertBefore(nonbreakingSpace2, null);
- return cursor;
+ return (
+ cursors.get(clientID) || createCursor(clientID, user.name, user.color)
+ ).element;
};
tiptapExtensions.push(
CollaborationCursor.configure({
diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css
index ba9dffb39d..8f413ad254 100644
--- a/packages/core/src/editor/editor.css
+++ b/packages/core/src/editor/editor.css
@@ -10,6 +10,15 @@
--N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */
}
+.bn-comment-editor {
+ width: 100%;
+ padding: 0;
+}
+
+.bn-comment-editor .bn-editor {
+ padding: 0;
+}
+
/*
bn-root should be applied to all top-level elements
@@ -83,7 +92,6 @@ Tippy popups that are appended to document.body directly
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
- pointer-events: none;
position: relative;
word-break: normal;
white-space: nowrap !important;
@@ -92,17 +100,33 @@ Tippy popups that are appended to document.body directly
/* Render the username above the caret */
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
- color: #0d0d0d;
font-size: 12px;
font-style: normal;
font-weight: 600;
- left: -1px;
line-height: normal;
- padding: 0.1rem 0.3rem;
+ left: -1px;
+ overflow: hidden;
position: absolute;
- top: -1.4em;
- user-select: none;
white-space: nowrap;
+
+ color: transparent;
+ max-height: 4px;
+ max-width: 4px;
+ padding: 0;
+ top: 0;
+
+ transition: all 0.2s;
+
+}
+
+.collaboration-cursor__caret[data-active] > .collaboration-cursor__label {
+ color: #0d0d0d;
+ max-height: 1.1rem;
+ max-width: 20rem;
+ padding: 0.1rem 0.3rem;
+ top: -14px;
+
+ transition: all 0.2s;
}
/* .tableWrapper {
diff --git a/packages/core/src/extensions/Comments/CommentMark.ts b/packages/core/src/extensions/Comments/CommentMark.ts
new file mode 100644
index 0000000000..3b4f2e8fee
--- /dev/null
+++ b/packages/core/src/extensions/Comments/CommentMark.ts
@@ -0,0 +1,45 @@
+import { Mark, mergeAttributes } from "@tiptap/core";
+
+export const CommentMark = Mark.create({
+ name: "comment",
+ excludes: "",
+ inclusive: false,
+ keepOnSplit: true,
+ group: "blocknoteIgnore", // ignore in blocknote json
+
+ addAttributes() {
+ // Return an object with attribute configuration
+ return {
+ // TODO: check if needed
+ orphan: {
+ parseHTML: (element) => !!element.getAttribute("data-orphan"),
+ renderHTML: (attributes) => {
+ return (attributes as { orphan: boolean }).orphan
+ ? {
+ "data-orphan": "true",
+ }
+ : {};
+ },
+ default: false,
+ },
+ threadId: {
+ parseHTML: (element) => element.getAttribute("data-bn-thread-id"),
+ renderHTML: (attributes) => {
+ return {
+ "data-bn-thread-id": (attributes as { threadId: string }).threadId,
+ };
+ },
+ default: "",
+ },
+ };
+ },
+
+ renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) {
+ return [
+ "span",
+ mergeAttributes(HTMLAttributes, {
+ class: "bn-thread-mark",
+ }),
+ ];
+ },
+});
diff --git a/packages/core/src/extensions/Comments/CommentsPlugin.ts b/packages/core/src/extensions/Comments/CommentsPlugin.ts
new file mode 100644
index 0000000000..ed71492dab
--- /dev/null
+++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts
@@ -0,0 +1,278 @@
+import { Node } from "prosemirror-model";
+import { Plugin, PluginKey } from "prosemirror-state";
+import { Decoration, DecorationSet } from "prosemirror-view";
+import * as Y from "yjs";
+import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
+import { EventEmitter } from "../../util/EventEmitter.js";
+import { DefaultThreadStoreAuth } from "./threadstore/DefaultThreadStoreAuth.js";
+import { ThreadStore } from "./threadstore/ThreadStore.js";
+import { YjsThreadStore } from "./threadstore/YjsThreadStore.js";
+import { CommentBody, ThreadData, User } from "./types.js";
+import { UserStore } from "./userstore/UserStore.js";
+const PLUGIN_KEY = new PluginKey(`blocknote-comments`);
+
+enum CommentsPluginActions {
+ SET_SELECTED_THREAD_ID = "SET_SELECTED_THREAD_ID",
+}
+
+type CommentsPluginAction = {
+ name: CommentsPluginActions;
+ data: string | null;
+};
+
+type CommentsPluginState = {
+ threadPositions: Map;
+ // selectedThreadId: string | null;
+ // selectedThreadPos: number | null;
+ decorations: DecorationSet;
+};
+
+function updateState(
+ doc: Node,
+ selectedThreadId: string | undefined,
+ markType: string
+) {
+ const threadPositions = new Map();
+ const decorations: Decoration[] = [];
+ // find all thread marks and store their position + create decoration for selected thread
+ doc.descendants((node, pos) => {
+ node.marks.forEach((mark) => {
+ if (mark.type.name === markType) {
+ const thisThreadId = (mark.attrs as { threadId: string | undefined })
+ .threadId;
+ if (!thisThreadId) {
+ return;
+ }
+ const from = pos;
+ const to = from + node.nodeSize;
+
+ // FloatingThreads component uses "to" as the position, so always store the largest "to" found
+ // AnchoredThreads component uses "from" as the position, so always store the smallest "from" found
+ const currentPosition = threadPositions.get(thisThreadId) ?? {
+ from: Infinity,
+ to: 0,
+ };
+ threadPositions.set(thisThreadId, {
+ from: Math.min(from, currentPosition.from),
+ to: Math.max(to, currentPosition.to),
+ });
+
+ if (selectedThreadId === thisThreadId) {
+ decorations.push(
+ Decoration.inline(from, to, {
+ class: "bn-thread-mark-selected",
+ })
+ );
+ }
+ }
+ });
+ });
+ return {
+ decorations: DecorationSet.create(doc, decorations),
+ threadPositions,
+ };
+}
+
+export class CommentsPlugin extends EventEmitter {
+ public readonly plugin: Plugin;
+ public readonly store: ThreadStore;
+ private pendingComment = false;
+ private selectedThreadId: string | undefined;
+
+ private emitStateUpdate() {
+ this.emit("update", {
+ selectedThreadId: this.selectedThreadId,
+ pendingComment: this.pendingComment,
+ });
+ }
+
+ /**
+ * when a thread is resolved or deleted, we need to update the marks to reflect the new state
+ */
+ private updateMarksFromThreads = (threads: Map) => {
+ const doc = new Y.Doc();
+ const threadMap = doc.getMap("threads");
+ threads.forEach((thread) => {
+ threadMap.set(thread.id, thread);
+ });
+
+ const ttEditor = this.editor._tiptapEditor;
+ if (!ttEditor) {
+ // TODO: better lifecycle management
+ return;
+ }
+
+ ttEditor.state.doc.descendants((node, pos) => {
+ node.marks.forEach((mark) => {
+ if (mark.type.name === this.markType) {
+ const markType = mark.type;
+ const markThreadId = mark.attrs.threadId;
+ const thread = threads.get(markThreadId);
+ const isOrphan = !thread || thread.resolved || thread.deletedAt;
+
+ if (isOrphan !== mark.attrs.orphan) {
+ const { tr } = ttEditor.state;
+ const trimmedFrom = Math.max(pos, 0);
+ const trimmedTo = Math.min(
+ pos + node.nodeSize,
+ ttEditor.state.doc.content.size - 1
+ );
+ tr.removeMark(trimmedFrom, trimmedTo, markType);
+ tr.addMark(
+ trimmedFrom,
+ trimmedTo,
+ markType.create({
+ ...mark.attrs,
+ orphan: isOrphan,
+ })
+ );
+ ttEditor.dispatch(tr);
+
+ if (isOrphan && this.selectedThreadId === markThreadId) {
+ // unselect
+ this.selectedThreadId = undefined;
+ this.emitStateUpdate();
+ }
+ }
+ }
+ });
+ });
+ };
+
+ constructor(
+ private readonly editor: BlockNoteEditor,
+ private readonly markType: string,
+ public readonly userStore = new UserStore(async (userIds) => {
+ // fake slow network request
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ // random username
+ const names = ["John Doe", "Jane Doe", "John Smith", "Jane Smith"];
+ const username = names[Math.floor(Math.random() * names.length)];
+
+ return userIds.map((id) => ({
+ id,
+ username,
+ avatarUrl: `https://placehold.co/100x100?text=${username}`,
+ }));
+ })
+ ) {
+ super();
+
+ const doc = new Y.Doc();
+ this.store = new YjsThreadStore(
+ editor,
+ "blablauserid",
+ doc.getMap("threads"),
+ new DefaultThreadStoreAuth("blablauserid", "comment")
+ );
+
+ // TODO: unsubscribe
+ this.store.subscribe(this.updateMarksFromThreads);
+
+ // initial
+ this.updateMarksFromThreads(this.store.getThreads());
+
+ // TODO: remove settimeout
+ setTimeout(() => {
+ editor.onSelectionChange(() => {
+ // TODO: filter out yjs transactions
+ if (this.pendingComment) {
+ this.pendingComment = false;
+ this.emitStateUpdate();
+ }
+ });
+ }, 600);
+
+ const self = this;
+
+ this.plugin = new Plugin({
+ key: PLUGIN_KEY,
+ state: {
+ init() {
+ return {
+ threadPositions: new Map(),
+ decorations: DecorationSet.empty,
+ } satisfies CommentsPluginState;
+ },
+ apply(tr, state) {
+ const action = tr.getMeta(PLUGIN_KEY) as CommentsPluginAction;
+ if (!tr.docChanged && !action) {
+ return state;
+ }
+
+ // Doc changed, but no action, just update rects
+ return updateState(tr.doc, self.selectedThreadId, markType);
+ },
+ },
+ props: {
+ decorations(state) {
+ return PLUGIN_KEY.getState(state)?.decorations ?? DecorationSet.empty;
+ },
+ handleClick: (view, pos, event) => {
+ if (event.button !== 0) {
+ return;
+ }
+
+ const selectThread = (threadId: string | undefined) => {
+ self.selectedThreadId = threadId;
+ self.emitStateUpdate();
+ view.dispatch(
+ view.state.tr.setMeta(PLUGIN_KEY, {
+ name: CommentsPluginActions.SET_SELECTED_THREAD_ID,
+ })
+ );
+ };
+
+ const node = view.state.doc.nodeAt(pos);
+ if (!node) {
+ selectThread(undefined);
+ return;
+ }
+ const commentMark = node.marks.find(
+ (mark) => mark.type.name === markType
+ );
+ // don't allow selecting orphaned threads
+ if (commentMark?.attrs.orphan) {
+ selectThread(undefined);
+ return;
+ }
+ const threadId = commentMark?.attrs.threadId as string | undefined;
+ selectThread(threadId);
+ },
+ },
+ });
+ }
+
+ public onUpdate(
+ callback: (state: {
+ pendingComment: boolean;
+ selectedThreadId: string | undefined;
+ }) => void
+ ) {
+ return this.on("update", callback);
+ }
+
+ public startPendingComment() {
+ this.pendingComment = true;
+ this.emitStateUpdate();
+ }
+
+ public stopPendingComment() {
+ this.pendingComment = false;
+ this.emitStateUpdate();
+ }
+
+ public async createThread(options: {
+ initialComment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ metadata?: any;
+ }) {
+ const thread = await this.store.createThread(options);
+ this.editor._tiptapEditor.commands.setMark(this.markType, {
+ threadId: thread.id,
+ });
+ }
+}
diff --git a/packages/core/src/extensions/Comments/threadstore/DefaultThreadStoreAuth.ts b/packages/core/src/extensions/Comments/threadstore/DefaultThreadStoreAuth.ts
new file mode 100644
index 0000000000..4faf240116
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/DefaultThreadStoreAuth.ts
@@ -0,0 +1,74 @@
+import { CommentData, ThreadData } from "../types.js";
+import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
+
+/*
+ * The methods are annotated with the recommended auth pattern
+ * (but of course this could be different in your app):
+ * - View-only users should not be able to see any comments
+ * - Comment-only users and editors can:
+ * - - create new comments / replies / reactions
+ * - - edit / delete their own comments / reactions
+ * - - resolve / unresolve threads
+ * - Editors can also delete any comment or thread
+ */
+export class DefaultThreadStoreAuth extends ThreadStoreAuth {
+ constructor(
+ private readonly userId: string,
+ private readonly role: "comment" | "editor"
+ ) {
+ super();
+ }
+
+ /**
+ * Auth: should be possible by anyone with comment access
+ */
+ canCreateThread(): boolean {
+ return true;
+ }
+
+ /**
+ * Auth: should be possible by anyone with comment access
+ */
+ canAddComment(_thread: ThreadData): boolean {
+ return true;
+ }
+
+ /**
+ * Auth: should only be possible by the comment author
+ */
+ canUpdateComment(comment: CommentData): boolean {
+ return comment.userId === this.userId;
+ }
+
+ /**
+ * Auth: should be possible by the comment author OR an editor of the document
+ */
+ canDeleteComment(comment: CommentData): boolean {
+ return comment.userId === this.userId || this.role === "editor";
+ }
+
+ /**
+ * Auth: should only be possible by an editor of the document
+ */
+ canDeleteThread(_thread: ThreadData): boolean {
+ return this.role === "editor";
+ }
+
+ /**
+ * Auth: should be possible by anyone with comment access
+ */
+ canResolveThread(_thread: ThreadData): boolean {
+ return true;
+ }
+
+ /**
+ * Auth: should be possible by anyone with comment access
+ */
+ canUnresolveThread(_thread: ThreadData): boolean {
+ return true;
+ }
+
+ // TODO: reactions
+ // abstract canAddReaction(comment: CommentData): boolean;
+ // abstract canDeleteReaction(comment: CommentData): boolean;
+}
diff --git a/packages/core/src/extensions/Comments/threadstore/LiveBlocksThreadStore.ts b/packages/core/src/extensions/Comments/threadstore/LiveBlocksThreadStore.ts
new file mode 100644
index 0000000000..d7b28a527a
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/LiveBlocksThreadStore.ts
@@ -0,0 +1,8 @@
+export class LiveblocksThreadStore {
+ constructor(private readonly editor: BlockNoteEditor) {}
+
+ public async createThread() {
+ const x = useCreateThread();
+ return x;
+ }
+}
diff --git a/packages/core/src/extensions/Comments/threadstore/ThreadStore.ts b/packages/core/src/extensions/Comments/threadstore/ThreadStore.ts
new file mode 100644
index 0000000000..01c2b4c75d
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/ThreadStore.ts
@@ -0,0 +1,101 @@
+import { CommentBody, CommentData, ThreadData } from "../types.js";
+import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
+
+/**
+ * ThreadStore is an abstract class that defines the interface
+ * to read / add / update / delete threads and comments.
+ */
+export abstract class ThreadStore {
+ public readonly auth: ThreadStoreAuth;
+
+ constructor(auth: ThreadStoreAuth) {
+ this.auth = auth;
+ }
+
+ /**
+ * Creates a new thread with an initial comment.
+ */
+ abstract createThread(options: {
+ initialComment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ metadata?: any;
+ }): Promise;
+
+ /**
+ * Adds a comment to a thread.
+ */
+ abstract addComment(options: {
+ comment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ threadId: string;
+ }): Promise;
+
+ /**
+ * Updates a comment in a thread.
+ */
+ abstract updateComment(options: {
+ comment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ threadId: string;
+ commentId: string;
+ }): Promise;
+
+ /**
+ * Deletes a comment from a thread.
+ */
+ abstract deleteComment(options: {
+ threadId: string;
+ commentId: string;
+ }): Promise;
+
+ /**
+ * Deletes a thread.
+ */
+ abstract deleteThread(options: { threadId: string }): Promise;
+
+ /**
+ * Marks a thread as resolved.
+ */
+ abstract resolveThread(options: { threadId: string }): Promise;
+
+ /**
+ * Marks a thread as unresolved.
+ */
+ abstract unresolveThread(options: { threadId: string }): Promise;
+
+ /**
+ * Adds a reaction to a comment.
+ *
+ * Auth: should be possible by anyone with comment access
+ */
+ abstract addReaction(options: {
+ threadId: string;
+ commentId: string;
+ reaction: string;
+ }): Promise;
+
+ /**
+ * Deletes a reaction from a comment.
+ *
+ * Auth: should be possible by the reaction author
+ */
+ abstract deleteReaction(options: {
+ threadId: string;
+ commentId: string;
+ reactionId: string;
+ }): Promise;
+
+ abstract getThread(threadId: string): ThreadData;
+
+ abstract getThreads(): Map;
+
+ abstract subscribe(
+ cb: (threads: Map) => void
+ ): () => void;
+}
diff --git a/packages/core/src/extensions/Comments/threadstore/ThreadStoreAuth.ts b/packages/core/src/extensions/Comments/threadstore/ThreadStoreAuth.ts
new file mode 100644
index 0000000000..57031c015c
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/ThreadStoreAuth.ts
@@ -0,0 +1,14 @@
+import { CommentData, ThreadData } from "../types.js";
+
+export abstract class ThreadStoreAuth {
+ abstract canCreateThread(): boolean;
+ abstract canAddComment(thread: ThreadData): boolean;
+ abstract canUpdateComment(comment: CommentData): boolean;
+ abstract canDeleteComment(comment: CommentData): boolean;
+ abstract canDeleteThread(thread: ThreadData): boolean;
+ abstract canResolveThread(thread: ThreadData): boolean;
+ abstract canUnresolveThread(thread: ThreadData): boolean;
+ // TODO: reactions
+ // abstract canAddReaction(comment: CommentData): boolean;
+ // abstract canDeleteReaction(comment: CommentData): boolean;
+}
diff --git a/packages/core/src/extensions/Comments/threadstore/TipTapThreadStore.ts b/packages/core/src/extensions/Comments/threadstore/TipTapThreadStore.ts
new file mode 100644
index 0000000000..8d068a37c5
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/TipTapThreadStore.ts
@@ -0,0 +1,7 @@
+export class TiptapThreadStore {
+ constructor(private readonly editor: BlockNoteEditor) {}
+
+ public async createThread() {
+ this.editor._tiptapEditor.commands.setMark(this.markType, { threadId: id });
+ }
+}
diff --git a/packages/core/src/extensions/Comments/threadstore/YjsThreadStore.test.ts b/packages/core/src/extensions/Comments/threadstore/YjsThreadStore.test.ts
new file mode 100644
index 0000000000..d3f939e57d
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/YjsThreadStore.test.ts
@@ -0,0 +1,282 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import * as Y from "yjs";
+import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import { CommentBody } from "../types.js";
+import { YjsThreadStore } from "./YjsThreadStore.js";
+
+// Mock UUID to generate sequential IDs
+let mockUuidCounter = 0;
+vi.mock("uuid", () => ({
+ v4: () => `mocked-uuid-${++mockUuidCounter}`,
+}));
+
+describe("YjsThreadStore", () => {
+ let store: YjsThreadStore;
+ let doc: Y.Doc;
+ let threadsYMap: Y.Map;
+ let editor: BlockNoteEditor;
+
+ beforeEach(() => {
+ // Reset mocks and create fresh instances
+ vi.clearAllMocks();
+ mockUuidCounter = 0;
+ doc = new Y.Doc();
+ threadsYMap = doc.getMap("threads");
+ editor = {} as BlockNoteEditor;
+ store = new YjsThreadStore(editor, "test-user", threadsYMap);
+ });
+
+ describe("createThread", () => {
+ it("creates a thread with initial comment", async () => {
+ const initialComment = {
+ body: "Test comment" as CommentBody,
+ metadata: { extra: "metadatacomment" },
+ };
+
+ const thread = await store.createThread({
+ initialComment,
+ metadata: { extra: "metadatathread" },
+ });
+
+ expect(thread).toMatchObject({
+ type: "thread",
+ id: "mocked-uuid-2",
+ resolved: false,
+ metadata: { extra: "metadatathread" },
+ comments: [
+ {
+ type: "comment",
+ id: "mocked-uuid-1",
+ userId: "test-user",
+ body: "Test comment",
+ metadata: { extra: "metadatacomment" },
+ reactions: [],
+ },
+ ],
+ });
+ });
+ });
+
+ describe("addComment", () => {
+ it("adds a comment to existing thread", async () => {
+ // First create a thread
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Initial comment" as CommentBody,
+ },
+ });
+
+ // Add new comment
+ const comment = await store.addComment({
+ threadId: thread.id,
+ comment: {
+ body: "New comment" as CommentBody,
+ metadata: { test: "metadata" },
+ },
+ });
+
+ expect(comment).toMatchObject({
+ type: "comment",
+ id: "mocked-uuid-3",
+ userId: "test-user",
+ body: "New comment",
+ metadata: { test: "metadata" },
+ reactions: [],
+ });
+
+ // Verify thread has both comments
+ const updatedThread = store.getThread(thread.id);
+ expect(updatedThread.comments).toHaveLength(2);
+ });
+
+ it("throws error for non-existent thread", async () => {
+ await expect(
+ store.addComment({
+ threadId: "non-existent",
+ comment: {
+ body: "Test comment" as CommentBody,
+ },
+ })
+ ).rejects.toThrow("Thread not found");
+ });
+ });
+
+ describe("updateComment", () => {
+ it("updates existing comment", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Initial comment" as CommentBody,
+ },
+ });
+
+ await store.updateComment({
+ threadId: thread.id,
+ commentId: thread.comments[0].id,
+ comment: {
+ body: "Updated comment" as CommentBody,
+ metadata: { updatedMetadata: true },
+ },
+ });
+
+ const updatedThread = store.getThread(thread.id);
+ expect(updatedThread.comments[0]).toMatchObject({
+ body: "Updated comment",
+ metadata: { updatedMetadata: true },
+ });
+ });
+ });
+
+ describe("deleteComment", () => {
+ it("soft deletes a comment", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await store.deleteComment({
+ threadId: thread.id,
+ commentId: thread.comments[0].id,
+ softDelete: true,
+ });
+
+ const updatedThread = store.getThread(thread.id);
+ expect(updatedThread.comments[0].deletedAt).toBeDefined();
+ expect(updatedThread.comments[0].body).toBeUndefined();
+ });
+
+ it("hard deletes a comment (deletes thread)", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await store.deleteComment({
+ threadId: thread.id,
+ commentId: thread.comments[0].id,
+ softDelete: false,
+ });
+
+ // Thread should be deleted since it was the only comment
+ expect(() => store.getThread(thread.id)).toThrow("Thread not found");
+ });
+ });
+
+ describe("resolveThread", () => {
+ it("resolves a thread", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await store.resolveThread({ threadId: thread.id });
+
+ const updatedThread = store.getThread(thread.id);
+ expect(updatedThread.resolved).toBe(true);
+ expect(updatedThread.resolvedUpdatedAt).toBeDefined();
+ });
+ });
+
+ describe("unresolveThread", () => {
+ it("unresolves a thread", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await store.resolveThread({ threadId: thread.id });
+ await store.unresolveThread({ threadId: thread.id });
+
+ const updatedThread = store.getThread(thread.id);
+ expect(updatedThread.resolved).toBe(false);
+ expect(updatedThread.resolvedUpdatedAt).toBeDefined();
+ });
+ });
+
+ describe("getThreads", () => {
+ it("returns all threads", async () => {
+ await store.createThread({
+ initialComment: {
+ body: "Thread 1" as CommentBody,
+ },
+ });
+
+ await store.createThread({
+ initialComment: {
+ body: "Thread 2" as CommentBody,
+ },
+ });
+
+ const threads = store.getThreads();
+ expect(threads.size).toBe(2);
+ });
+ });
+
+ describe("deleteThread", () => {
+ it("deletes an entire thread", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await store.deleteThread({ threadId: thread.id });
+
+ // Verify thread is deleted
+ expect(() => store.getThread(thread.id)).toThrow("Thread not found");
+ });
+ });
+
+ describe("reactions", () => {
+ it("throws not implemented error when adding reaction", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await expect(
+ store.addReaction({
+ threadId: thread.id,
+ commentId: thread.comments[0].id,
+ })
+ ).rejects.toThrow("Not implemented");
+ });
+
+ it("throws not implemented error when deleting reaction", async () => {
+ const thread = await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ await expect(
+ store.deleteReaction({
+ threadId: thread.id,
+ commentId: thread.comments[0].id,
+ reactionId: "some-reaction",
+ })
+ ).rejects.toThrow("Not implemented");
+ });
+ });
+
+ describe("subscribe", () => {
+ it("calls callback when threads change", async () => {
+ const callback = vi.fn();
+ const unsubscribe = store.subscribe(callback);
+
+ await store.createThread({
+ initialComment: {
+ body: "Test comment" as CommentBody,
+ },
+ });
+
+ expect(callback).toHaveBeenCalled();
+
+ unsubscribe();
+ });
+ });
+});
diff --git a/packages/core/src/extensions/Comments/threadstore/YjsThreadStore.ts b/packages/core/src/extensions/Comments/threadstore/YjsThreadStore.ts
new file mode 100644
index 0000000000..aea14335f3
--- /dev/null
+++ b/packages/core/src/extensions/Comments/threadstore/YjsThreadStore.ts
@@ -0,0 +1,452 @@
+import { v4 } from "uuid";
+import * as Y from "yjs";
+import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import {
+ CommentBody,
+ CommentData,
+ CommentReactionData,
+ ThreadData,
+} from "../types.js";
+import { ThreadStore } from "./ThreadStore.js";
+import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
+
+export class YjsThreadStore extends ThreadStore {
+ constructor(
+ private readonly editor: BlockNoteEditor,
+ private readonly userId: string,
+ private readonly threadsYMap: Y.Map,
+ auth: ThreadStoreAuth
+ ) {
+ super(auth);
+ }
+
+ private transact = (
+ fn: (options: T) => R
+ ): ((options: T) => Promise) => {
+ return async (options: T) => {
+ return this.threadsYMap.doc!.transact(() => {
+ return fn(options);
+ });
+ };
+ };
+
+ public createThread = this.transact(
+ (options: {
+ initialComment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ metadata?: any;
+ }) => {
+ if (!this.auth.canCreateThread()) {
+ throw new Error("Not authorized");
+ }
+
+ const date = new Date();
+
+ const comment: CommentData = {
+ type: "comment",
+ id: v4(),
+ userId: this.userId,
+ createdAt: date,
+ updatedAt: date,
+ reactions: [],
+ metadata: options.initialComment.metadata,
+ body: options.initialComment.body,
+ };
+
+ const thread: ThreadData = {
+ type: "thread",
+ id: v4(),
+ createdAt: date,
+ updatedAt: date,
+ comments: [comment],
+ resolved: false,
+ metadata: options.metadata,
+ };
+
+ this.threadsYMap.set(thread.id, threadToYMap(thread));
+
+ return thread;
+ }
+ );
+
+ public addComment = this.transact(
+ (options: {
+ comment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ threadId: string;
+ }) => {
+ const yThread = this.threadsYMap.get(options.threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+
+ if (!this.auth.canAddComment(yMapToThread(yThread))) {
+ throw new Error("Not authorized");
+ }
+
+ const date = new Date();
+ const comment: CommentData = {
+ type: "comment",
+ id: v4(),
+ userId: this.userId,
+ createdAt: date,
+ updatedAt: date,
+ deletedAt: undefined,
+ reactions: [],
+ metadata: options.comment.metadata,
+ body: options.comment.body,
+ };
+
+ (yThread.get("comments") as Y.Array>).push([
+ commentToYMap(comment),
+ ]);
+
+ yThread.set("updatedAt", new Date().getTime());
+ return comment;
+ }
+ );
+
+ public updateComment = this.transact(
+ (options: {
+ comment: {
+ body: CommentBody;
+ metadata?: any;
+ };
+ threadId: string;
+ commentId: string;
+ }) => {
+ const yThread = this.threadsYMap.get(options.threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+
+ const yCommentIndex = yArrayFindIndex(
+ yThread.get("comments"),
+ (comment) => comment.get("id") === options.commentId
+ );
+
+ if (yCommentIndex === -1) {
+ throw new Error("Comment not found");
+ }
+
+ const yComment = yThread.get("comments").get(yCommentIndex);
+
+ if (!this.auth.canUpdateComment(yMapToComment(yComment))) {
+ throw new Error("Not authorized");
+ }
+
+ yComment.set("body", options.comment.body);
+ yComment.set("updatedAt", new Date().getTime());
+ yComment.set("metadata", options.comment.metadata);
+ }
+ );
+
+ public deleteComment = this.transact(
+ (options: {
+ threadId: string;
+ commentId: string;
+ softDelete?: boolean;
+ }) => {
+ const yThread = this.threadsYMap.get(options.threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+
+ const yCommentIndex = yArrayFindIndex(
+ yThread.get("comments"),
+ (comment) => comment.get("id") === options.commentId
+ );
+
+ if (yCommentIndex === -1) {
+ throw new Error("Comment not found");
+ }
+
+ const yComment = yThread.get("comments").get(yCommentIndex);
+
+ if (!this.auth.canDeleteComment(yMapToComment(yComment))) {
+ throw new Error("Not authorized");
+ }
+
+ if (yComment.get("deletedAt")) {
+ throw new Error("Comment already deleted");
+ }
+
+ if (options.softDelete) {
+ yComment.set("deletedAt", new Date().getTime());
+ yComment.set("body", undefined);
+ } else {
+ yThread.get("comments").delete(yCommentIndex);
+ }
+
+ if (
+ (yThread.get("comments") as Y.Array)
+ .toArray()
+ .every((comment) => comment.get("deletedAt"))
+ ) {
+ // all comments deleted
+ if (options.softDelete) {
+ yThread.set("deletedAt", new Date().getTime());
+ } else {
+ this.threadsYMap.delete(options.threadId);
+ }
+ }
+
+ yThread.set("updatedAt", new Date().getTime());
+ }
+ );
+
+ public deleteThread = this.transact((options: { threadId: string }) => {
+ if (
+ !this.auth.canDeleteThread(
+ yMapToThread(this.threadsYMap.get(options.threadId))
+ )
+ ) {
+ throw new Error("Not authorized");
+ }
+
+ this.threadsYMap.delete(options.threadId);
+ });
+
+ public resolveThread = this.transact((options: { threadId: string }) => {
+ const yThread = this.threadsYMap.get(options.threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+
+ if (!this.auth.canResolveThread(yMapToThread(yThread))) {
+ throw new Error("Not authorized");
+ }
+
+ yThread.set("resolved", true);
+ yThread.set("resolvedUpdatedAt", new Date().getTime());
+ });
+
+ public unresolveThread = this.transact((options: { threadId: string }) => {
+ const yThread = this.threadsYMap.get(options.threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+
+ if (!this.auth.canUnresolveThread(yMapToThread(yThread))) {
+ throw new Error("Not authorized");
+ }
+
+ yThread.set("resolved", false);
+ yThread.set("resolvedUpdatedAt", new Date().getTime());
+ });
+
+ public toggleReaction = this.transact(
+ (options: { threadId: string; commentId: string; reaction: string }) => {
+ const yThread = this.threadsYMap.get(options.threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+
+ const yCommentIndex = yArrayFindIndex(
+ yThread.get("comments"),
+ (comment) => comment.get("id") === options.commentId
+ );
+ if (yCommentIndex === -1) {
+ throw new Error("Comment not found");
+ }
+
+ const yComment = yThread.get("comments").get(yCommentIndex);
+ if (!this.auth.canUpdateComment(yMapToComment(yComment))) {
+ throw new Error("Not authorized");
+ }
+
+ const yReactionIndex = yArrayFindIndex(
+ yComment.get("reactions"),
+ (reaction) => reaction.get("emoji") === options.reaction
+ );
+
+ if (yReactionIndex !== -1) {
+ const yReaction = yComment.get("reactions").get(yReactionIndex);
+
+ const yUserIdIndex = yArrayFindIndex(
+ yReaction.get("usersIds"),
+ (userId) => userId === this.userId
+ );
+ if (yUserIdIndex !== -1) {
+ // This user already reacted with this emoji, so it should be toggled
+ // off.
+
+ // Reaction exists and contains user ID, so the user ID should be
+ // removed from the list. If the list is now empty, remove the reaction
+ // altogether.
+ yReaction.get("usersIds").delete(yUserIdIndex);
+
+ if (yReaction.get("usersIds").length === 0) {
+ yComment.get("reactions").delete(yReactionIndex);
+ yComment.set("updatedAt", new Date().getTime());
+ }
+
+ return;
+ }
+ // Other users have reacted with this emoji, but this user has not, so
+ // it should be toggled on.
+
+ // Reaction exists but does not contain user ID, so the user ID should
+ // be added to the list.
+ yReaction.get("usersIds").push([this.userId]);
+
+ return;
+ }
+ // No one has reacted with this emoji, so it should again be toggled on.
+
+ // Reaction does not exist, and so a new one should be created and this
+ // user should be added to the list.
+ const date = new Date();
+ const reaction: CommentReactionData = {
+ emoji: options.reaction,
+ createdAt: date,
+ usersIds: [this.userId],
+ };
+
+ yComment.get("reactions").push([reactionToYMap(reaction)]);
+ yComment.set("updatedAt", date.getTime());
+ }
+ );
+
+ // TODO: async / reactive interface?
+ public getThread(threadId: string) {
+ const yThread = this.threadsYMap.get(threadId);
+ if (!yThread) {
+ throw new Error("Thread not found");
+ }
+ const thread = yMapToThread(yThread);
+ return thread;
+ }
+
+ public getThreads(): Map {
+ const threadMap = new Map();
+ this.threadsYMap.forEach((yThread, id) => {
+ threadMap.set(id, yMapToThread(yThread));
+ });
+ return threadMap;
+ }
+
+ public subscribe(cb: (threads: Map) => void) {
+ const observer = () => {
+ cb(this.getThreads());
+ };
+
+ this.threadsYMap.observeDeep(observer);
+
+ return () => {
+ this.threadsYMap.unobserveDeep(observer);
+ };
+ }
+}
+
+// HELPERS
+
+function reactionToYMap(reaction: CommentReactionData) {
+ const yMap = new Y.Map();
+ yMap.set("emoji", reaction.emoji);
+ yMap.set("createdAt", reaction.createdAt.getTime());
+ if (reaction.usersIds.length === 0) {
+ throw new Error("Need at least one user ID in reactionToYMap");
+ }
+ const usersIdsArray = new Y.Array();
+
+ usersIdsArray.push([...reaction.usersIds]);
+
+ yMap.set("usersIds", usersIdsArray);
+
+ return yMap;
+}
+
+function commentToYMap(comment: CommentData) {
+ const yMap = new Y.Map();
+ yMap.set("id", comment.id);
+ yMap.set("userId", comment.userId);
+ yMap.set("createdAt", comment.createdAt.getTime());
+ yMap.set("updatedAt", comment.updatedAt.getTime());
+ if (comment.deletedAt) {
+ yMap.set("deletedAt", comment.deletedAt.getTime());
+ yMap.set("body", undefined);
+ } else {
+ yMap.set("body", comment.body);
+ }
+ if (comment.reactions.length > 0) {
+ throw new Error("Reactions should be empty in commentToYMap");
+ }
+ yMap.set("reactions", new Y.Array());
+ yMap.set("metadata", comment.metadata);
+
+ return yMap;
+}
+
+function threadToYMap(thread: ThreadData) {
+ const yMap = new Y.Map();
+ yMap.set("id", thread.id);
+ yMap.set("createdAt", thread.createdAt.getTime());
+ yMap.set("updatedAt", thread.updatedAt.getTime());
+ const commentsArray = new Y.Array>();
+
+ commentsArray.push(thread.comments.map((comment) => commentToYMap(comment)));
+
+ yMap.set("comments", commentsArray);
+ yMap.set("resolved", thread.resolved);
+ yMap.set("resolvedUpdatedAt", thread.resolvedUpdatedAt?.getTime());
+ yMap.set("metadata", thread.metadata);
+ return yMap;
+}
+
+function yMapToReaction(yMap: Y.Map): CommentReactionData {
+ return {
+ emoji: yMap.get("emoji"),
+ createdAt: yMap.get("createdAt"),
+ usersIds: yMap.get("usersIds").toArray(),
+ };
+}
+
+function yMapToComment(yMap: Y.Map): CommentData {
+ return {
+ type: "comment",
+ id: yMap.get("id"),
+ userId: yMap.get("userId"),
+ createdAt: new Date(yMap.get("createdAt")),
+ updatedAt: new Date(yMap.get("updatedAt")),
+ deletedAt: yMap.get("deletedAt")
+ ? new Date(yMap.get("deletedAt"))
+ : undefined,
+ reactions: yMap
+ .get("reactions")
+ .map((reaction: Y.Map) => yMapToReaction(reaction)),
+ metadata: yMap.get("metadata"),
+ body: yMap.get("body"),
+ };
+}
+
+function yMapToThread(yMap: Y.Map): ThreadData {
+ return {
+ type: "thread",
+ id: yMap.get("id"),
+ createdAt: new Date(yMap.get("createdAt")),
+ updatedAt: new Date(yMap.get("updatedAt")),
+ comments: ((yMap.get("comments") as Y.Array>) || []).map(
+ (comment) => yMapToComment(comment)
+ ),
+ resolved: yMap.get("resolved"),
+ resolvedUpdatedAt: yMap.get("resolvedUpdatedAt"),
+ metadata: yMap.get("metadata"),
+ };
+}
+
+function yArrayFindIndex(
+ yArray: Y.Array,
+ predicate: (item: any) => boolean
+) {
+ for (let i = 0; i < yArray.length; i++) {
+ if (predicate(yArray.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+}
diff --git a/packages/core/src/extensions/Comments/types.ts b/packages/core/src/extensions/Comments/types.ts
new file mode 100644
index 0000000000..c50f769ec3
--- /dev/null
+++ b/packages/core/src/extensions/Comments/types.ts
@@ -0,0 +1,45 @@
+export type CommentBody = any;
+
+export type CommentReactionData = {
+ emoji: string;
+ createdAt: Date;
+ usersIds: string[];
+};
+
+export type CommentData = {
+ type: "comment";
+ id: string;
+ userId: string;
+ createdAt: Date;
+ updatedAt: Date;
+ reactions: CommentReactionData[];
+ // attachments: CommentAttachment[];
+ metadata: any;
+} & (
+ | {
+ deletedAt: Date;
+ body: undefined;
+ }
+ | {
+ deletedAt?: never;
+ body: CommentBody;
+ }
+);
+
+export type ThreadData = {
+ type: "thread";
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ comments: CommentData[];
+ resolved: boolean;
+ resolvedUpdatedAt?: Date;
+ metadata: any;
+ deletedAt?: Date;
+};
+
+export type User = {
+ id: string;
+ username: string;
+ avatarUrl: string;
+};
diff --git a/packages/core/src/extensions/Comments/userstore/UserStore.ts b/packages/core/src/extensions/Comments/userstore/UserStore.ts
new file mode 100644
index 0000000000..4cad7326e0
--- /dev/null
+++ b/packages/core/src/extensions/Comments/userstore/UserStore.ts
@@ -0,0 +1,48 @@
+import { EventEmitter } from "../../../util/EventEmitter.js";
+import { User } from "../types.js";
+export class UserStore extends EventEmitter {
+ private userCache: Map = new Map();
+
+ // avoid duplicate loads
+ private loadingUsers = new Set();
+
+ public constructor(
+ private readonly resolveUsers: (userIds: string[]) => Promise
+ ) {
+ super();
+ }
+
+ public async loadUsers(userIds: string[]) {
+ const missingUsers = userIds.filter(
+ (id) => !this.userCache.has(id) && !this.loadingUsers.has(id)
+ );
+
+ if (missingUsers.length === 0) {
+ return;
+ }
+
+ for (const id of missingUsers) {
+ this.loadingUsers.add(id);
+ }
+
+ try {
+ const users = await this.resolveUsers(missingUsers);
+ for (const user of users) {
+ this.userCache.set(user.id, user);
+ }
+ this.emit("update", this.userCache);
+ } finally {
+ for (const id of missingUsers) {
+ this.loadingUsers.delete(id);
+ }
+ }
+ }
+
+ public getUser(userId: string): U | undefined {
+ return this.userCache.get(userId);
+ }
+
+ public subscribe(cb: (users: Map) => void): () => void {
+ return this.on("update", cb);
+ }
+}
diff --git a/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts b/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts
index 440457eb29..db1681e89d 100644
--- a/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts
+++ b/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts
@@ -1,5 +1,6 @@
import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
+import { v4 } from "uuid";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
@@ -12,7 +13,9 @@ export class PlaceholderPlugin {
) {
this.plugin = new Plugin({
key: PLUGIN_KEY,
- view: () => {
+ view: (view) => {
+ const uniqueEditorSelector = `placeholder-selector-${v4()}`;
+ view.dom.classList.add(uniqueEditorSelector);
const styleEl = document.createElement("style");
const nonce = editor._tiptapEditor.options.injectNonce;
if (nonce) {
@@ -27,7 +30,7 @@ export class PlaceholderPlugin {
const styleSheet = styleEl.sheet!;
const getBaseSelector = (additionalSelectors = "") =>
- `.bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;
+ `.${uniqueEditorSelector} .bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;
const getSelector = (
blockType: string | "default",
diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
index eb50f42487..4013a8846a 100644
--- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
+++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
@@ -1,6 +1,6 @@
-import { PluginView } from "@tiptap/pm/state";
-import { EditorState, Plugin, PluginKey } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
+import { DOMParser, Slice } from "@tiptap/pm/model";
+import { EditorState, Plugin, PluginKey, PluginView } from "@tiptap/pm/state";
+import { EditorView } from "@tiptap/pm/view";
import { Block } from "../../blocks/defaultBlocks.js";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
@@ -14,6 +14,7 @@ import { EventEmitter } from "../../util/EventEmitter.js";
import { initializeESMDependencies } from "../../util/esmDependencies.js";
import { getDraggableBlockFromElement } from "../getDraggableBlockFromElement.js";
import { dragStart, unsetDragImage } from "./dragging.js";
+
export type SideMenuState<
BSchema extends BlockSchema,
I extends InlineContentSchema,
@@ -28,9 +29,14 @@ const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1;
function getBlockFromCoords(
view: EditorView,
coords: { left: number; top: number },
+ sideMenuDetection: "viewport" | "editor",
adjustForColumns = true
) {
- const elements = view.root.elementsFromPoint(coords.left, coords.top);
+ const elements = view.root.elementsFromPoint(
+ // bit hacky - offset x position to right to account for the width of sidemenu itself
+ coords.left + (sideMenuDetection === "editor" ? 50 : 0),
+ coords.top
+ );
for (const element of elements) {
if (!view.dom.contains(element)) {
@@ -46,6 +52,7 @@ function getBlockFromCoords(
left: coords.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself
top: coords.top,
},
+ sideMenuDetection,
false
);
}
@@ -60,7 +67,8 @@ function getBlockFromMousePos(
x: number;
y: number;
},
- view: EditorView
+ view: EditorView,
+ sideMenuDetection: "viewport" | "editor"
): { node: HTMLElement; id: string } | undefined {
// Editor itself may have padding or other styling which affects
// size/position, so we get the boundingRect of the first child (i.e. the
@@ -76,7 +84,7 @@ function getBlockFromMousePos(
// this.horizontalPosAnchor = editorBoundingBox.x;
- // Gets block at mouse cursor's vertical position.
+ // Gets block at mouse cursor's position.
const coords = {
left: mousePos.x,
top: mousePos.y,
@@ -85,15 +93,18 @@ function getBlockFromMousePos(
const mouseLeftOfEditor = coords.left < editorBoundingBox.left;
const mouseRightOfEditor = coords.left > editorBoundingBox.right;
- if (mouseLeftOfEditor) {
- coords.left = editorBoundingBox.left + 10;
- }
+ // Clamps the x position to the editor's bounding box.
+ if (sideMenuDetection === "viewport") {
+ if (mouseLeftOfEditor) {
+ coords.left = editorBoundingBox.left + 10;
+ }
- if (mouseRightOfEditor) {
- coords.left = editorBoundingBox.right - 10;
+ if (mouseRightOfEditor) {
+ coords.left = editorBoundingBox.right - 10;
+ }
}
- let block = getBlockFromCoords(view, coords);
+ let block = getBlockFromCoords(view, coords, sideMenuDetection);
if (!mouseRightOfEditor && block) {
// note: this case is not necessary when we're on the right side of the editor
@@ -101,14 +112,14 @@ function getBlockFromMousePos(
/* Now, because blocks can be nested
| BlockA |
x | BlockB y|
-
+
hovering over position x (the "margin of block B") will return block A instead of block B.
to fix this, we get the block from the right side of block A (position y, which will fall in BlockB correctly)
*/
const rect = block.node.getBoundingClientRect();
coords.left = rect.right - 10;
- block = getBlockFromCoords(view, coords, false);
+ block = getBlockFromCoords(view, coords, "viewport", false);
}
return block;
@@ -132,8 +143,11 @@ export class SideMenuView<
public menuFrozen = false;
+ public isDragOrigin = false;
+
constructor(
private readonly editor: BlockNoteEditor,
+ private readonly sideMenuDetection: "viewport" | "editor",
private readonly pmView: EditorView,
emitUpdate: (state: SideMenuState) => void
) {
@@ -146,14 +160,18 @@ export class SideMenuView<
};
this.pmView.root.addEventListener(
- "drop",
- this.onDrop as EventListener,
- true
+ "dragstart",
+ this.onDragStart as EventListener
);
this.pmView.root.addEventListener(
"dragover",
this.onDragOver as EventListener
);
+ this.pmView.root.addEventListener(
+ "drop",
+ this.onDrop as EventListener,
+ true
+ );
initializeESMDependencies();
// Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
@@ -169,6 +187,11 @@ export class SideMenuView<
this.onKeyDown as EventListener,
true
);
+
+ // Setting capture=true ensures that any parent container of the editor that
+ // gets scrolled will trigger the scroll event. Scroll events do not bubble
+ // and so won't propagate to the document by default.
+ pmView.root.addEventListener("scroll", this.onScroll, true);
}
updateState = (state: SideMenuState) => {
@@ -181,7 +204,11 @@ export class SideMenuView<
return;
}
- const block = getBlockFromMousePos(this.mousePos, this.pmView);
+ const block = getBlockFromMousePos(
+ this.mousePos,
+ this.pmView,
+ this.sideMenuDetection
+ );
// Closes the menu if the mouse cursor is beyond the editor vertically.
if (!block || !this.editor.isEditable) {
@@ -249,7 +276,16 @@ export class SideMenuView<
onDrop = (event: DragEvent) => {
this.editor._tiptapEditor.commands.blur();
+ // ProseMirror doesn't remove the dragged content if it's dropped outside
+ // the editor (e.g. to other editors), so we need to do it manually. Since
+ // the dragged content is the same as the selected content, we can just
+ // delete the selection.
+ if (this.isDragOrigin && !this.pmView.dom.contains(event.target as Node)) {
+ this.pmView.dispatch(this.pmView.state.tr.deleteSelection());
+ }
+
if (
+ this.sideMenuDetection === "editor" ||
(event as any).synthetic ||
!event.dataTransfer?.types.includes("blocknote/html")
) {
@@ -268,6 +304,46 @@ export class SideMenuView<
}
};
+ /**
+ * If a block is being dragged, ProseMirror usually gets the context of what's
+ * being dragged from `view.dragging`, which is automatically set when a
+ * `dragstart` event fires in the editor. However, if the user tries to drag
+ * and drop blocks between multiple editors, only the one in which the drag
+ * began has that context, so we need to set it on the others manually. This
+ * ensures that PM always drops the blocks in between other blocks, and not
+ * inside them.
+ *
+ * After the `dragstart` event fires on the drag handle, it sets
+ * `blocknote/html` data on the clipboard. This handler fires right after,
+ * parsing the `blocknote/html` data into nodes and setting them on
+ * `view.dragging`.
+ *
+ * Note: Setting `view.dragging` on `dragover` would be better as the user
+ * could then drag between editors in different windows, but you can only
+ * access `dataTransfer` contents on `dragstart` and `drop` events.
+ */
+ onDragStart = (event: DragEvent) => {
+ if (!this.pmView.dragging) {
+ const html = event.dataTransfer?.getData("blocknote/html");
+ if (!html) {
+ return;
+ }
+
+ const element = document.createElement("div");
+ element.innerHTML = html;
+
+ const parser = DOMParser.fromSchema(this.pmView.state.schema);
+ const node = parser.parse(element, {
+ topNode: this.pmView.state.schema.nodes["blockGroup"].create(),
+ });
+
+ this.pmView.dragging = {
+ slice: new Slice(node.content, 0, 0),
+ move: true,
+ };
+ }
+ };
+
/**
* If the event is outside the editor contents,
* we dispatch a fake event, so that we can still drop the content
@@ -275,11 +351,13 @@ export class SideMenuView<
*/
onDragOver = (event: DragEvent) => {
if (
+ this.sideMenuDetection === "editor" ||
(event as any).synthetic ||
!event.dataTransfer?.types.includes("blocknote/html")
) {
return;
}
+
const pos = this.pmView.posAtCoords({
left: event.clientX,
top: event.clientY,
@@ -400,6 +478,13 @@ export class SideMenuView<
return evt;
}
+ onScroll = () => {
+ if (this.state?.show) {
+ this.state.referencePos = this.hoveredBlock!.getBoundingClientRect();
+ this.emitUpdate(this.state);
+ }
+ };
+
// Needed in cases where the editor state updates without the mouse cursor
// moving, as some state updates can require a side menu update. For example,
// adding a button to the side menu which removes the block can cause the
@@ -424,11 +509,14 @@ export class SideMenuView<
this.onMouseMove as EventListener,
true
);
+ this.pmView.root.removeEventListener(
+ "dragstart",
+ this.onDragStart as EventListener
+ );
this.pmView.root.removeEventListener(
"dragover",
this.onDragOver as EventListener
);
-
this.pmView.root.removeEventListener(
"drop",
this.onDrop as EventListener,
@@ -439,6 +527,7 @@ export class SideMenuView<
this.onKeyDown as EventListener,
true
);
+ this.pmView.root.removeEventListener("scroll", this.onScroll, true);
}
}
@@ -452,14 +541,22 @@ export class SideMenuProsemirrorPlugin<
public view: SideMenuView | undefined;
public readonly plugin: Plugin;
- constructor(private readonly editor: BlockNoteEditor) {
+ constructor(
+ private readonly editor: BlockNoteEditor,
+ sideMenuDetection: "viewport" | "editor"
+ ) {
super();
this.plugin = new Plugin({
key: sideMenuPluginKey,
view: (editorView) => {
- this.view = new SideMenuView(editor, editorView, (state) => {
- this.emit("update", state);
- });
+ this.view = new SideMenuView(
+ editor,
+ sideMenuDetection,
+ editorView,
+ (state) => {
+ this.emit("update", state);
+ }
+ );
return this.view;
},
});
@@ -479,6 +576,10 @@ export class SideMenuProsemirrorPlugin<
},
block: Block
) => {
+ if (this.view) {
+ this.view.isDragOrigin = true;
+ }
+
dragStart(event, block, this.editor);
};
@@ -489,6 +590,10 @@ export class SideMenuProsemirrorPlugin<
if (this.editor.prosemirrorView) {
unsetDragImage(this.editor.prosemirrorView.root);
}
+
+ if (this.view) {
+ this.view.isDragOrigin = false;
+ }
};
/**
* Freezes the side menu. When frozen, the side menu will stay
diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts
index 2c8a4bb5d1..1dba4f462e 100644
--- a/packages/core/src/extensions/SideMenu/dragging.ts
+++ b/packages/core/src/extensions/SideMenu/dragging.ts
@@ -202,6 +202,5 @@ export function dragStart<
e.dataTransfer.setData("text/plain", plainText);
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setDragImage(dragImageElement!, 0, 0);
- view.dragging = { slice: selectedSlice, move: true };
}
}
diff --git a/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts b/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts
index f2e11d5ee8..6789b76616 100644
--- a/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts
+++ b/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts
@@ -1,7 +1,7 @@
import type { Dictionary } from "../../i18n/dictionary.js";
export type DefaultSuggestionItem = {
- key: keyof Dictionary["slash_menu"];
+ key: keyof Omit;
title: string;
onItemClick: () => void;
subtext?: string;
diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts
index b18a202599..c4c2ad8a8e 100644
--- a/packages/core/src/i18n/locales/ar.ts
+++ b/packages/core/src/i18n/locales/ar.ts
@@ -58,6 +58,12 @@ export const ar: Dictionary = {
aliases: ["كود", "مسبق"],
group: "الكتل الأساسية",
},
+ page_break: {
+ title: "فاصل الصفحة",
+ subtext: "فاصل الصفحة",
+ aliases: ["page", "break", "separator", "فاصل", "الصفحة"],
+ group: "الكتل الأساسية",
+ },
table: {
title: "جدول",
subtext: "يستخدم للجداول",
diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts
index 6331d722e7..f4b9e5f5f0 100644
--- a/packages/core/src/i18n/locales/de.ts
+++ b/packages/core/src/i18n/locales/de.ts
@@ -56,6 +56,12 @@ export const de = {
aliases: ["code", "pre"],
group: "Grundlegende blöcke",
},
+ page_break: {
+ title: "Seitenumbruch",
+ subtext: "Seitentrenner",
+ aliases: ["page", "break", "separator", "seitenumbruch", "trenner"],
+ group: "Grundlegende Blöcke",
+ },
table: {
title: "Tabelle",
subtext: "Tabelle mit editierbaren Zellen",
diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts
index 6365542b4e..9ef4b4e800 100644
--- a/packages/core/src/i18n/locales/en.ts
+++ b/packages/core/src/i18n/locales/en.ts
@@ -56,6 +56,12 @@ export const en = {
aliases: ["code", "pre"],
group: "Basic blocks",
},
+ page_break: {
+ title: "Page Break",
+ subtext: "Page separator",
+ aliases: ["page", "break", "separator"],
+ group: "Basic blocks",
+ },
table: {
title: "Table",
subtext: "Table with editable cells",
diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts
index 2ffc5587fc..0a8e11ac9f 100644
--- a/packages/core/src/i18n/locales/es.ts
+++ b/packages/core/src/i18n/locales/es.ts
@@ -55,6 +55,12 @@ export const es = {
aliases: ["code", "pre"],
group: "Bloques básicos",
},
+ page_break: {
+ title: "Salto de página",
+ subtext: "Separador de página",
+ aliases: ["page", "break", "separator", "salto", "separador"],
+ group: "Bloques básicos",
+ },
table: {
title: "Tabla",
subtext: "Tabla con celdas editables",
diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts
index 152d2805dd..9f694dadb6 100644
--- a/packages/core/src/i18n/locales/fr.ts
+++ b/packages/core/src/i18n/locales/fr.ts
@@ -10,13 +10,14 @@ export const fr: Dictionary = {
},
heading_2: {
title: "Titre 2",
- subtext: "Utilisé pour les sections clés",
+ subtext: "Titre de deuxième niveau Utilisé pour les sections clés",
aliases: ["h2", "titre2", "sous-titre"],
group: "Titres",
},
heading_3: {
title: "Titre 3",
- subtext: "Utilisé pour les sous-sections et les titres de groupe",
+ subtext:
+ "Titre de troisième niveau utilisé pour les sous-sections et les titres de groupe",
aliases: ["h3", "titre3", "sous-titre"],
group: "Titres",
},
@@ -27,13 +28,21 @@ export const fr: Dictionary = {
group: "Blocs de base",
},
bullet_list: {
- title: "Liste à Puces",
- subtext: "Utilisé pour afficher une liste non ordonnée",
- aliases: ["ul", "li", "liste", "listeàpuces", "liste à puces"],
+ title: "Liste à puces",
+ subtext: "Utilisé pour afficher une liste à puce non numérotée",
+ aliases: [
+ "ul",
+ "li",
+ "liste",
+ "listeàpuces",
+ "liste à puces",
+ "bullet points",
+ "bulletpoints",
+ ],
group: "Blocs de base",
},
check_list: {
- title: "Liste de vérification",
+ title: "Liste de tâches",
subtext: "Utilisé pour afficher une liste avec des cases à cocher",
aliases: [
"ul",
@@ -42,13 +51,18 @@ export const fr: Dictionary = {
"liste de vérification",
"liste cochée",
"case à cocher",
+ "checklist",
+ "checkbox",
+ "check box",
+ "to do",
+ "todo",
],
group: "Blocs de base",
},
paragraph: {
title: "Paragraphe",
subtext: "Utilisé pour le corps de votre document",
- aliases: ["p", "paragraphe"],
+ aliases: ["p", "paragraphe", "texte"],
group: "Blocs de base",
},
code_block: {
@@ -57,10 +71,16 @@ export const fr: Dictionary = {
aliases: ["code", "pre"],
group: "Blocs de base",
},
+ page_break: {
+ title: "Saut de page",
+ subtext: "Séparateur de page",
+ aliases: ["page", "break", "separator", "saut", "séparateur"],
+ group: "Blocs de base",
+ },
table: {
title: "Tableau",
subtext: "Utilisé pour les tableaux",
- aliases: ["tableau"],
+ aliases: ["tableau", "grille"],
group: "Avancé",
},
image: {
@@ -69,7 +89,9 @@ export const fr: Dictionary = {
aliases: [
"image",
"uploadImage",
- "télécharger",
+ "télécharger image",
+ "téléverser image",
+ "uploader image",
"img",
"photo",
"média",
@@ -82,8 +104,8 @@ export const fr: Dictionary = {
subtext: "Insérer une vidéo",
aliases: [
"vidéo",
- "téléchargerVidéo",
- "téléverser",
+ "télécharger vidéo",
+ "téléverser vidéo",
"mp4",
"film",
"média",
@@ -96,8 +118,8 @@ export const fr: Dictionary = {
subtext: "Insérer un audio",
aliases: [
"audio",
- "téléchargerAudio",
- "téléverser",
+ "télécharger audio",
+ "téléverser audio",
"mp3",
"son",
"média",
@@ -108,18 +130,26 @@ export const fr: Dictionary = {
file: {
title: "Fichier",
subtext: "Insérer un fichier",
- aliases: ["fichier", "téléverser", "intégrer", "média", "url"],
+ aliases: [
+ "fichier",
+ "téléverser fichier",
+ "intégrer fichier",
+ "insérer fichier",
+ "média",
+ "url",
+ ],
group: "Média",
},
emoji: {
title: "Emoji",
subtext: "Utilisé pour insérer un emoji",
- aliases: ["emoji", "émoticône", "émotion", "visage"],
+ aliases: ["emoji", "émoticône", "émotion", "visage", "smiley"],
group: "Autres",
},
},
placeholders: {
- default: "Entrez du texte ou tapez '/' pour les commandes",
+ default:
+ "Entrez du texte ou tapez '/' pour faire apparaître les options de mise en page",
heading: "Titre",
bulletListItem: "Liste",
numberedListItem: "Liste",
@@ -280,7 +310,7 @@ export const fr: Dictionary = {
audio: "Télécharger un fichier audio",
file: "Télécharger un fichier",
},
- upload_error: "Erreur : Échec du téléchargement",
+ upload_error: "Erreur : échec du téléchargement",
},
embed: {
title: "Intégrer",
diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts
index 5b65fc5468..b7604e6ff8 100644
--- a/packages/core/src/i18n/locales/hr.ts
+++ b/packages/core/src/i18n/locales/hr.ts
@@ -21,26 +21,38 @@ export const hr = {
numbered_list: {
title: "Numerirani popis",
subtext: "Popis s numeriranim stavkama",
- aliases: ["poredaniPopis", "stavkaPopisa", "popis", "numeriraniPopis", "numerirani popis"],
+ aliases: [
+ "poredaniPopis",
+ "stavkaPopisa",
+ "popis",
+ "numeriraniPopis",
+ "numerirani popis",
+ ],
group: "Osnovni blokovi",
},
bullet_list: {
title: "Popis s oznakama",
subtext: "Popis s grafičkim oznakama",
- aliases: ["neporedaniPopis", "stavkaPopisa", "popis", "popisSOznakama", "popis s oznakama"],
+ aliases: [
+ "neporedaniPopis",
+ "stavkaPopisa",
+ "popis",
+ "popisSOznakama",
+ "popis s oznakama",
+ ],
group: "Osnovni blokovi",
},
check_list: {
title: "Check lista",
subtext: "Popis s kućicama za označavanje",
aliases: [
- "neporedaniPopis",
- "stavkaPopisa",
- "popis",
- "popisZaProvjeru",
- "check lista",
- "označeni popis",
- "kućicaZaOznačavanje",
+ "neporedaniPopis",
+ "stavkaPopisa",
+ "popis",
+ "popisZaProvjeru",
+ "check lista",
+ "označeni popis",
+ "kućicaZaOznačavanje",
],
group: "Osnovni blokovi",
},
@@ -56,17 +68,23 @@ export const hr = {
aliases: ["tablica"],
group: "Napredno",
},
+ page_break: {
+ title: "Prijelom stranice",
+ subtext: "Razdjelnik stranice",
+ aliases: ["page", "break", "separator", "prijelom", "razdjelnik"],
+ group: "Osnovni blokovi",
+ },
image: {
title: "Slika",
subtext: "Slika s podesivom veličinom i natpisom",
aliases: [
- "slika",
- "učitavanjeSlike",
- "učitaj",
- "img",
- "fotografija",
- "medij",
- "url",
+ "slika",
+ "učitavanjeSlike",
+ "učitaj",
+ "img",
+ "fotografija",
+ "medij",
+ "url",
],
group: "Mediji",
},
@@ -74,13 +92,13 @@ export const hr = {
title: "Video",
subtext: "Video s podesivom veličinom i natpisom",
aliases: [
- "video",
- "učitavanjeVidea",
- "učitaj",
- "mp4",
- "film",
- "medij",
- "url",
+ "video",
+ "učitavanjeVidea",
+ "učitaj",
+ "mp4",
+ "film",
+ "medij",
+ "url",
],
group: "Mediji",
},
@@ -88,13 +106,13 @@ export const hr = {
title: "Audio",
subtext: "Audio s natpisom",
aliases: [
- "audio",
- "učitavanjeAudija",
- "učitaj",
- "mp3",
- "zvuk",
- "medij",
- "url",
+ "audio",
+ "učitavanjeAudija",
+ "učitaj",
+ "mp3",
+ "zvuk",
+ "medij",
+ "url",
],
group: "Mediji",
},
@@ -157,16 +175,16 @@ export const hr = {
text_title: "Tekst",
background_title: "Pozadina",
colors: {
- default: "Zadano",
- gray: "Siva",
- brown: "Smeđa",
- red: "Crvena",
- orange: "Narančasta",
- yellow: "Žuta",
- green: "Zelena",
- blue: "Plava",
- purple: "Ljubičasta",
- pink: "Ružičasta",
+ default: "Zadano",
+ gray: "Siva",
+ brown: "Smeđa",
+ red: "Crvena",
+ orange: "Narančasta",
+ yellow: "Žuta",
+ green: "Zelena",
+ blue: "Plava",
+ purple: "Ljubičasta",
+ pink: "Ružičasta",
},
},
@@ -240,29 +258,29 @@ export const hr = {
file: "Ukloni datoteku",
} as Record,
},
- file_preview_toggle: {
+ file_preview_toggle: {
tooltip: "Prikaži/sakrij pregled",
- },
- nest: {
+ },
+ nest: {
tooltip: "Ugnijezdi blok",
secondary_tooltip: "Tab",
- },
- unnest: {
+ },
+ unnest: {
tooltip: "Razgnijezdi blok",
secondary_tooltip: "Shift+Tab",
- },
- align_left: {
+ },
+ align_left: {
tooltip: "Poravnaj tekst lijevo",
- },
- align_center: {
+ },
+ align_center: {
tooltip: "Poravnaj tekst po sredini",
- },
- align_right: {
+ },
+ align_right: {
tooltip: "Poravnaj tekst desno",
- },
- align_justify: {
+ },
+ align_justify: {
tooltip: "Poravnaj tekst obostrano",
- },
+ },
},
file_panel: {
upload: {
diff --git a/packages/core/src/i18n/locales/index.ts b/packages/core/src/i18n/locales/index.ts
index d17fc75f24..cead59d687 100644
--- a/packages/core/src/i18n/locales/index.ts
+++ b/packages/core/src/i18n/locales/index.ts
@@ -13,3 +13,4 @@ export * from "./pt.js";
export * from "./ru.js";
export * from "./vi.js";
export * from "./zh.js";
+export * from "./it.js"
\ No newline at end of file
diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts
index 07fcb3bfb7..978ba481ba 100644
--- a/packages/core/src/i18n/locales/is.ts
+++ b/packages/core/src/i18n/locales/is.ts
@@ -50,6 +50,12 @@ export const is: Dictionary = {
aliases: ["kóði", "pre"],
group: "Grunnblokkar",
},
+ page_break: {
+ title: "Síðubrot",
+ subtext: "Síðuskil",
+ aliases: ["page", "break", "separator", "síðubrot", "síðuskil"],
+ group: "Grunnblokkir",
+ },
table: {
title: "Tafla",
subtext: "Notað fyrir töflur",
diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts
new file mode 100644
index 0000000000..b1df6e780e
--- /dev/null
+++ b/packages/core/src/i18n/locales/it.ts
@@ -0,0 +1,315 @@
+export const it = {
+ slash_menu: {
+ heading: {
+ title: "Intestazione 1",
+ subtext: "Intestazione di primo livello",
+ aliases: ["h", "intestazione1", "h1"],
+ group: "Intestazioni",
+ },
+ heading_2: {
+ title: "Intestazione 2",
+ subtext: "Intestazione di sezione chiave",
+ aliases: ["h2", "intestazione2", "sottotitolo"],
+ group: "Intestazioni",
+ },
+ heading_3: {
+ title: "Intestazione 3",
+ subtext: "Intestazione di sottosezione e gruppo",
+ aliases: ["h3", "intestazione3", "sottotitolo"],
+ group: "Intestazioni",
+ },
+ numbered_list: {
+ title: "Elenco Numerato",
+ subtext: "Elenco con elementi ordinati",
+ aliases: ["ol", "li", "elenco", "elenconumerato", "elenco numerato"],
+ group: "Blocchi Base",
+ },
+ bullet_list: {
+ title: "Elenco Puntato",
+ subtext: "Elenco con elementi non ordinati",
+ aliases: ["ul", "li", "elenco", "elencopuntato", "elenco puntato"],
+ group: "Blocchi Base",
+ },
+ check_list: {
+ title: "Elenco di Controllo",
+ subtext: "Elenco con caselle di controllo",
+ aliases: [
+ "ul",
+ "li",
+ "elenco",
+ "elencocontrollo",
+ "elenco controllo",
+ "elenco verificato",
+ "casella di controllo",
+ ],
+ group: "Blocchi Base",
+ },
+ paragraph: {
+ title: "Paragrafo",
+ subtext: "Il corpo del tuo documento",
+ aliases: ["p", "paragrafo"],
+ group: "Blocchi Base",
+ },
+ code_block: {
+ title: "Blocco di Codice",
+ subtext: "Blocco di codice con evidenziazione della sintassi",
+ aliases: ["code", "pre"],
+ group: "Blocchi Base",
+ },
+ table: {
+ title: "Tabella",
+ subtext: "Tabella con celle modificabili",
+ aliases: ["tabella"],
+ group: "Avanzato",
+ },
+ image: {
+ title: "Immagine",
+ subtext: "Immagine ridimensionabile con didascalia",
+ aliases: [
+ "immagine",
+ "caricaImmagine",
+ "carica",
+ "img",
+ "foto",
+ "media",
+ "url",
+ ],
+ group: "Media",
+ },
+ video: {
+ title: "Video",
+ subtext: "Video ridimensionabile con didascalia",
+ aliases: [
+ "video",
+ "caricaVideo",
+ "carica",
+ "mp4",
+ "film",
+ "media",
+ "url",
+ ],
+ group: "Media",
+ },
+ audio: {
+ title: "Audio",
+ subtext: "Audio incorporato con didascalia",
+ aliases: [
+ "audio",
+ "caricaAudio",
+ "carica",
+ "mp3",
+ "suono",
+ "media",
+ "url",
+ ],
+ group: "Media",
+ },
+ file: {
+ title: "File",
+ subtext: "File incorporato",
+ aliases: ["file", "carica", "embed", "media", "url"],
+ group: "Media",
+ },
+ emoji: {
+ title: "Emoji",
+ subtext: "Cerca e inserisci un'emoji",
+ aliases: ["emoji", "emote", "emozione", "faccia"],
+ group: "Altri",
+ },
+ },
+ placeholders: {
+ default: "Inserisci testo o digita '/' per i comandi",
+ heading: "Intestazione",
+ bulletListItem: "Elenco",
+ numberedListItem: "Elenco",
+ checkListItem: "Elenco",
+ },
+ file_blocks: {
+ image: {
+ add_button_text: "Aggiungi immagine",
+ },
+ video: {
+ add_button_text: "Aggiungi video",
+ },
+ audio: {
+ add_button_text: "Aggiungi audio",
+ },
+ file: {
+ add_button_text: "Aggiungi file",
+ },
+ },
+ // from react package:
+ side_menu: {
+ add_block_label: "Aggiungi blocco",
+ drag_handle_label: "Apri menu blocco",
+ },
+ drag_handle: {
+ delete_menuitem: "Elimina",
+ colors_menuitem: "Colori",
+ },
+ table_handle: {
+ delete_column_menuitem: "Elimina colonna",
+ delete_row_menuitem: "Elimina riga",
+ add_left_menuitem: "Aggiungi colonna a sinistra",
+ add_right_menuitem: "Aggiungi colonna a destra",
+ add_above_menuitem: "Aggiungi riga sopra",
+ add_below_menuitem: "Aggiungi riga sotto",
+ },
+ suggestion_menu: {
+ no_items_title: "Nessun elemento trovato",
+ loading: "Caricamento…",
+ },
+ color_picker: {
+ text_title: "Testo",
+ background_title: "Sfondo",
+ colors: {
+ default: "Predefinito",
+ gray: "Grigio",
+ brown: "Marrone",
+ red: "Rosso",
+ orange: "Arancione",
+ yellow: "Giallo",
+ green: "Verde",
+ blue: "Blu",
+ purple: "Viola",
+ pink: "Rosa",
+ },
+ },
+
+ formatting_toolbar: {
+ bold: {
+ tooltip: "Grassetto",
+ secondary_tooltip: "Cmd+B",
+ },
+ italic: {
+ tooltip: "Corsivo",
+ secondary_tooltip: "Cmd+I",
+ },
+ underline: {
+ tooltip: "Sottolineato",
+ secondary_tooltip: "Cmd+U",
+ },
+ strike: {
+ tooltip: "Barrato",
+ secondary_tooltip: "Cmd+Shift+S",
+ },
+ code: {
+ tooltip: "Codice",
+ secondary_tooltip: "",
+ },
+ colors: {
+ tooltip: "Colori",
+ },
+ link: {
+ tooltip: "Crea link",
+ secondary_tooltip: "Cmd+K",
+ },
+ file_caption: {
+ tooltip: "Modifica didascalia",
+ input_placeholder: "Modifica didascalia",
+ },
+ file_replace: {
+ tooltip: {
+ image: "Sostituisci immagine",
+ video: "Sostituisci video",
+ audio: "Sostituisci audio",
+ file: "Sostituisci file",
+ } as Record,
+ },
+ file_rename: {
+ tooltip: {
+ image: "Rinomina immagine",
+ video: "Rinomina video",
+ audio: "Rinomina audio",
+ file: "Rinomina file",
+ } as Record,
+ input_placeholder: {
+ image: "Rinomina immagine",
+ video: "Rinomina video",
+ audio: "Rinomina audio",
+ file: "Rinomina file",
+ } as Record,
+ },
+ file_download: {
+ tooltip: {
+ image: "Scarica immagine",
+ video: "Scarica video",
+ audio: "Scarica audio",
+ file: "Scarica file",
+ } as Record,
+ },
+ file_delete: {
+ tooltip: {
+ image: "Elimina immagine",
+ video: "Elimina video",
+ audio: "Elimina audio",
+ file: "Elimina file",
+ } as Record,
+ },
+ file_preview_toggle: {
+ tooltip: "Attiva/disattiva anteprima",
+ },
+ nest: {
+ tooltip: "Annida blocco",
+ secondary_tooltip: "Tab",
+ },
+ unnest: {
+ tooltip: "Disannida blocco",
+ secondary_tooltip: "Shift+Tab",
+ },
+ align_left: {
+ tooltip: "Allinea testo a sinistra",
+ },
+ align_center: {
+ tooltip: "Allinea testo al centro",
+ },
+ align_right: {
+ tooltip: "Allinea testo a destra",
+ },
+ align_justify: {
+ tooltip: "Giustifica testo",
+ },
+ },
+ file_panel: {
+ upload: {
+ title: "Carica",
+ file_placeholder: {
+ image: "Carica immagine",
+ video: "Carica video",
+ audio: "Carica audio",
+ file: "Carica file",
+ } as Record,
+ upload_error: "Errore: Caricamento fallito",
+ },
+ embed: {
+ title: "Incorpora",
+ embed_button: {
+ image: "Incorpora immagine",
+ video: "Incorpora video",
+ audio: "Incorpora audio",
+ file: "Incorpora file",
+ } as Record,
+ url_placeholder: "Inserisci URL",
+ },
+ },
+ link_toolbar: {
+ delete: {
+ tooltip: "Rimuovi link",
+ },
+ edit: {
+ text: "Modifica link",
+ tooltip: "Modifica",
+ },
+ open: {
+ tooltip: "Apri in una nuova scheda",
+ },
+ form: {
+ title_placeholder: "Modifica titolo",
+ url_placeholder: "Modifica URL",
+ },
+ },
+ generic: {
+ ctrl_shortcut: "Ctrl",
+ },
+ };
+
\ No newline at end of file
diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts
index 2172289b0b..a4c2ff04a4 100644
--- a/packages/core/src/i18n/locales/ja.ts
+++ b/packages/core/src/i18n/locales/ja.ts
@@ -74,6 +74,12 @@ export const ja: Dictionary = {
aliases: ["code", "pre", "コード", "コードブロック"],
group: "基本ブロック",
},
+ page_break: {
+ title: "改ページ",
+ subtext: "ページ区切り",
+ aliases: ["page", "break", "separator", "改ページ", "区切り"],
+ group: "基本ブロック",
+ },
table: {
title: "表",
subtext: "表に使用",
diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts
index cc0144481d..0bf8e24a1f 100644
--- a/packages/core/src/i18n/locales/ko.ts
+++ b/packages/core/src/i18n/locales/ko.ts
@@ -58,6 +58,12 @@ export const ko: Dictionary = {
aliases: ["code", "pre"],
group: "기본 블록",
},
+ page_break: {
+ title: "페이지 나누기",
+ subtext: "페이지 구분자",
+ aliases: ["page", "break", "separator", "페이지", "구분자"],
+ group: "기본 블록",
+ },
table: {
title: "표",
subtext: "간단한 표를 추가합니다.",
diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts
index 152e640001..25c3426d2f 100644
--- a/packages/core/src/i18n/locales/nl.ts
+++ b/packages/core/src/i18n/locales/nl.ts
@@ -50,6 +50,12 @@ export const nl: Dictionary = {
aliases: ["code", "pre"],
group: "Basisblokken",
},
+ page_break: {
+ title: "Pagina-einde",
+ subtext: "Paginascheiding",
+ aliases: ["page", "break", "separator", "pagina", "scheiding"],
+ group: "Basisblokken",
+ },
table: {
title: "Tabel",
subtext: "Gebruikt voor tabellen",
diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts
index fdf53fe591..befa6ace7c 100644
--- a/packages/core/src/i18n/locales/pl.ts
+++ b/packages/core/src/i18n/locales/pl.ts
@@ -50,6 +50,12 @@ export const pl: Dictionary = {
aliases: ["kod", "pre"],
group: "Podstawowe bloki",
},
+ page_break: {
+ title: "Podział strony",
+ subtext: "Separator strony",
+ aliases: ["page", "break", "separator", "podział", "separator"],
+ group: "Podstawowe bloki",
+ },
table: {
title: "Tabela",
subtext: "Używana do tworzenia tabel",
diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts
index 0f435b8955..6dd7dab3ca 100644
--- a/packages/core/src/i18n/locales/pt.ts
+++ b/packages/core/src/i18n/locales/pt.ts
@@ -57,6 +57,12 @@ export const pt: Dictionary = {
aliases: ["codigo", "pre"],
group: "Blocos básicos",
},
+ page_break: {
+ title: "Quebra de página",
+ subtext: "Separador de página",
+ aliases: ["page", "break", "separator", "quebra", "separador"],
+ group: "Blocos básicos",
+ },
table: {
title: "Tabela",
subtext: "Usado para tabelas",
diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts
index 54afd7aeb2..60309d5d90 100644
--- a/packages/core/src/i18n/locales/ru.ts
+++ b/packages/core/src/i18n/locales/ru.ts
@@ -75,6 +75,12 @@ export const ru: Dictionary = {
aliases: ["code", "pre", "блок кода"],
group: "Базовые блоки",
},
+ page_break: {
+ title: "Разрыв страницы",
+ subtext: "Разделитель страницы",
+ aliases: ["page", "break", "separator", "разрыв", "разделитель"],
+ group: "Основные блоки",
+ },
table: {
title: "Таблица",
subtext: "Используется для таблиц",
diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts
index 507146013f..eb1dbf31a2 100644
--- a/packages/core/src/i18n/locales/vi.ts
+++ b/packages/core/src/i18n/locales/vi.ts
@@ -57,6 +57,12 @@ export const vi: Dictionary = {
aliases: ["code", "pre"],
group: "Khối cơ bản",
},
+ page_break: {
+ title: "Ngắt trang",
+ subtext: "Phân cách trang",
+ aliases: ["page", "break", "separator", "ngắt", "phân cách"],
+ group: "Khối cơ bản",
+ },
table: {
title: "Bảng",
subtext: "Sử dụng để tạo bảng",
diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts
index 955b9ca098..cfb32c155f 100644
--- a/packages/core/src/i18n/locales/zh.ts
+++ b/packages/core/src/i18n/locales/zh.ts
@@ -75,6 +75,12 @@ export const zh: Dictionary = {
aliases: ["code", "pre", "代码", "预格式"],
group: "基础",
},
+ page_break: {
+ title: "分页符",
+ subtext: "页面分隔符",
+ aliases: ["page", "break", "separator", "分页", "分隔符"],
+ group: "基础",
+ },
table: {
title: "表格",
subtext: "使用表格",
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 7b0115ae47..0274513444 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,4 +1,5 @@
import * as locales from "./i18n/locales/index.js";
+export * from "./api/blockManipulation/commands/updateBlock/updateBlock.js";
export * from "./api/exporters/html/externalHTMLExporter.js";
export * from "./api/exporters/html/internalHTMLSerializer.js";
export * from "./api/getBlockInfoFromPos.js";
@@ -6,6 +7,9 @@ export * from "./api/nodeUtil.js";
export * from "./api/testUtil/index.js";
export * from "./blocks/AudioBlockContent/AudioBlockContent.js";
export * from "./blocks/CodeBlockContent/CodeBlockContent.js";
+export * from "./blocks/PageBreakBlockContent/PageBreakBlockContent.js";
+export * from "./blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.js";
+export * from "./blocks/PageBreakBlockContent/schema.js";
export * from "./blocks/FileBlockContent/FileBlockContent.js";
export * from "./blocks/FileBlockContent/helpers/parse/parseEmbedElement.js";
export * from "./blocks/FileBlockContent/helpers/parse/parseFigureElement.js";
@@ -53,7 +57,6 @@ export * from "./util/string.js";
export * from "./util/typescript.js";
export { UnreachableCaseError, assertEmpty } from "./util/typescript.js";
export { locales };
-export * from "./api/blockManipulation/commands/updateBlock/updateBlock.js";
// for testing from react (TODO: move):
export * from "./api/nodeConversions/blockToNode.js";
@@ -65,3 +68,5 @@ export * from "./extensions/UniqueID/UniqueID.js";
export * from "./api/exporters/markdown/markdownExporter.js";
export * from "./api/parsers/html/parseHTML.js";
export * from "./api/parsers/markdown/parseMarkdown.js";
+
+export * from "./extensions/Comments/types.js";
diff --git a/packages/core/src/util/browser.ts b/packages/core/src/util/browser.ts
index 9ecdf3250d..2a0a2905bd 100644
--- a/packages/core/src/util/browser.ts
+++ b/packages/core/src/util/browser.ts
@@ -12,7 +12,7 @@ export function formatKeyboardShortcut(shortcut: string, ctrlText = "Ctrl") {
}
}
-export function mergeCSSClasses(...classes: string[]) {
+export function mergeCSSClasses(...classes: (string | false | undefined)[]) {
return classes.filter((c) => c).join(" ");
}
diff --git a/packages/dev-scripts/package.json b/packages/dev-scripts/package.json
index 66478c08df..2412e043ad 100644
--- a/packages/dev-scripts/package.json
+++ b/packages/dev-scripts/package.json
@@ -3,7 +3,7 @@
"homepage": "https://github.com/TypeCellOS/BlockNote",
"private": true,
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.0",
"description": "",
"type": "module",
"scripts": {
diff --git a/packages/mantine/package.json b/packages/mantine/package.json
index 25e62ac976..588fdc0620 100644
--- a/packages/mantine/package.json
+++ b/packages/mantine/package.json
@@ -6,7 +6,7 @@
"*.css"
],
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -50,8 +50,8 @@
"clean": "rimraf dist && rimraf types"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@mantine/core": "^7.10.1",
"@mantine/hooks": "^7.10.1",
"@mantine/utils": "^6.0.21",
diff --git a/packages/mantine/src/BlockNoteView.tsx b/packages/mantine/src/BlockNoteView.tsx
new file mode 100644
index 0000000000..f9403d6a10
--- /dev/null
+++ b/packages/mantine/src/BlockNoteView.tsx
@@ -0,0 +1,100 @@
+import {
+ BlockSchema,
+ InlineContentSchema,
+ mergeCSSClasses,
+ StyleSchema,
+} from "@blocknote/core";
+import {
+ BlockNoteViewProps,
+ BlockNoteViewRaw,
+ ComponentsContext,
+ useBlockNoteContext,
+ usePrefersColorScheme,
+} from "@blocknote/react";
+import { MantineProvider } from "@mantine/core";
+import { useCallback } from "react";
+
+import {
+ applyBlockNoteCSSVariablesFromTheme,
+ removeBlockNoteCSSVariables,
+ Theme,
+} from "./BlockNoteTheme.js";
+import { components } from "./components.js";
+import "./style.css";
+export * from "./BlockNoteTheme.js";
+export * from "./defaultThemes.js";
+
+const mantineTheme = {
+ // Removes button press effect
+ activeClassName: "",
+};
+
+export const BlockNoteView = <
+ BSchema extends BlockSchema,
+ ISchema extends InlineContentSchema,
+ SSchema extends StyleSchema
+>(
+ props: Omit, "theme"> & {
+ theme?:
+ | "light"
+ | "dark"
+ | Theme
+ | {
+ light: Theme;
+ dark: Theme;
+ };
+ }
+) => {
+ const { className, theme, ...rest } = props;
+
+ const existingContext = useBlockNoteContext();
+ const systemColorScheme = usePrefersColorScheme();
+ const defaultColorScheme =
+ existingContext?.colorSchemePreference || systemColorScheme;
+
+ const ref = useCallback(
+ (node: HTMLDivElement | null) => {
+ if (!node) {
+ // todo: clean variables?
+ return;
+ }
+
+ removeBlockNoteCSSVariables(node);
+
+ if (typeof theme === "object") {
+ if ("light" in theme && "dark" in theme) {
+ applyBlockNoteCSSVariablesFromTheme(
+ theme[defaultColorScheme === "dark" ? "dark" : "light"],
+ node
+ );
+ return;
+ }
+
+ applyBlockNoteCSSVariablesFromTheme(theme, node);
+ return;
+ }
+ },
+ [defaultColorScheme, theme]
+ );
+
+ return (
+
+ {/* `cssVariablesSelector` scopes Mantine CSS variables to only the editor, */}
+ {/* as proposed here: https://github.com/orgs/mantinedev/discussions/5685 */}
+ undefined}>
+
+
+
+ );
+};
diff --git a/packages/mantine/src/badge/Badge.tsx b/packages/mantine/src/badge/Badge.tsx
new file mode 100644
index 0000000000..213a05316f
--- /dev/null
+++ b/packages/mantine/src/badge/Badge.tsx
@@ -0,0 +1,76 @@
+import { forwardRef } from "react";
+import { ComponentProps } from "@blocknote/react";
+import {
+ Chip as MantineChip,
+ Group as MantineGroup,
+ Tooltip as MantineTooltip,
+} from "@mantine/core";
+import { assertEmpty } from "@blocknote/core";
+
+import { TooltipContent } from "../toolbar/ToolbarButton.js";
+
+export const Badge = forwardRef<
+ HTMLInputElement,
+ ComponentProps["Generic"]["Badge"]["Root"]
+>((props, ref) => {
+ const {
+ className,
+ text,
+ icon,
+ isSelected,
+ mainTooltip,
+ secondaryTooltip,
+ onClick,
+ ...rest
+ } = props;
+
+ // false, because rest props can be added by mantine when chip is used as a trigger
+ // assertEmpty in this case is only used at typescript level, not runtime level
+ assertEmpty(rest, false);
+
+ const badge = (
+
+ {icon}
+ {text}
+
+ );
+
+ if (!mainTooltip) {
+ return badge;
+ }
+
+ return (
+
+ }
+ multiline>
+ {badge}
+
+ );
+});
+
+export const BadgeGroup = forwardRef<
+ HTMLDivElement,
+ ComponentProps["Generic"]["Badge"]["Group"]
+>((props, ref) => {
+ const { className, children, ...rest } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/mantine/src/comments/Card.tsx b/packages/mantine/src/comments/Card.tsx
new file mode 100644
index 0000000000..38865105ae
--- /dev/null
+++ b/packages/mantine/src/comments/Card.tsx
@@ -0,0 +1,34 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { Card as MantineCard } from "@mantine/core";
+import { forwardRef } from "react";
+
+export const Card = forwardRef<
+ HTMLDivElement,
+ ComponentProps["Comments"]["Card"]
+>((props, ref) => {
+ const { className, children, ...rest } = props;
+
+ assertEmpty(rest, false);
+
+ return (
+
+ {children}
+
+ );
+});
+
+export const CardSection = forwardRef<
+ HTMLDivElement,
+ ComponentProps["Comments"]["CardSection"]
+>((props, ref) => {
+ const { className, children, ...rest } = props;
+
+ assertEmpty(rest, false);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/mantine/src/comments/Comment.tsx b/packages/mantine/src/comments/Comment.tsx
new file mode 100644
index 0000000000..9167c09360
--- /dev/null
+++ b/packages/mantine/src/comments/Comment.tsx
@@ -0,0 +1,88 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps, mergeRefs } from "@blocknote/react";
+import { Avatar, Group, Skeleton, Text } from "@mantine/core";
+import { useHover } from "@mantine/hooks";
+import { forwardRef } from "react";
+
+const AuthorInfo = forwardRef<
+ HTMLDivElement,
+ Pick
+>((props, ref) => {
+ const { authorInfo, timeString, ...rest } = props;
+
+ assertEmpty(rest, false);
+
+ if (authorInfo === "loading") {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {authorInfo.username}
+
+ {timeString}
+
+
+
+ );
+});
+
+export const Comment = forwardRef<
+ HTMLDivElement,
+ ComponentProps["Comments"]["Comment"]
+>((props, ref) => {
+ const {
+ className,
+ showActions,
+ authorInfo,
+ timeString,
+ actions,
+ children,
+ ...rest
+ } = props;
+
+ const { hovered, ref: hoverRef } = useHover();
+ const mergedRef = mergeRefs([ref, hoverRef]);
+ assertEmpty(rest, false);
+
+ const doShowActions =
+ actions &&
+ (showActions === true ||
+ showActions === undefined ||
+ (showActions === "hover" && hovered));
+
+ return (
+
+ {doShowActions ? (
+
+ {actions}
+
+ ) : null}
+
+ {children}
+
+ );
+});
diff --git a/packages/mantine/src/comments/Editor.tsx b/packages/mantine/src/comments/Editor.tsx
new file mode 100644
index 0000000000..6bd4805b8f
--- /dev/null
+++ b/packages/mantine/src/comments/Editor.tsx
@@ -0,0 +1,37 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef, useEffect } from "react";
+import { BlockNoteView } from "../BlockNoteView.js";
+
+export const Editor = forwardRef<
+ HTMLDivElement,
+ ComponentProps["Comments"]["Editor"]
+>((props, ref) => {
+ const { className, onFocus, onBlur, editor, editable, ...rest } = props;
+
+ assertEmpty(rest, false);
+
+ // When we click the edit button on a comment, we also want to focus the
+ // comment editor
+ useEffect(() => {
+ if (editable) {
+ editor.focus();
+ }
+ }, [editable, editor]);
+
+ return (
+
+ );
+});
diff --git a/packages/mantine/src/components.tsx b/packages/mantine/src/components.tsx
new file mode 100644
index 0000000000..f39e4b89c3
--- /dev/null
+++ b/packages/mantine/src/components.tsx
@@ -0,0 +1,113 @@
+import { Components } from "@blocknote/react";
+
+import { Badge, BadgeGroup } from "./badge/Badge.js";
+import { Card, CardSection } from "./comments/Card.js";
+import { Comment } from "./comments/Comment.js";
+import { Editor } from "./comments/Editor.js";
+import { TextInput } from "./form/TextInput.js";
+import {
+ Menu,
+ MenuDivider,
+ MenuDropdown,
+ MenuItem,
+ MenuLabel,
+ MenuTrigger,
+} from "./menu/Menu.js";
+import { Panel } from "./panel/Panel.js";
+import { PanelButton } from "./panel/PanelButton.js";
+import { PanelFileInput } from "./panel/PanelFileInput.js";
+import { PanelTab } from "./panel/PanelTab.js";
+import { PanelTextInput } from "./panel/PanelTextInput.js";
+import { Popover, PopoverContent, PopoverTrigger } from "./popover/Popover.js";
+import { SideMenu } from "./sideMenu/SideMenu.js";
+import { SideMenuButton } from "./sideMenu/SideMenuButton.js";
+import "./style.css";
+import { SuggestionMenu } from "./suggestionMenu/SuggestionMenu.js";
+import { SuggestionMenuEmptyItem } from "./suggestionMenu/SuggestionMenuEmptyItem.js";
+import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem.js";
+import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel.js";
+import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader.js";
+import { GridSuggestionMenu } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.js";
+import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.js";
+import { GridSuggestionMenuItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.js";
+import { GridSuggestionMenuLoader } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.js";
+import { ExtendButton } from "./tableHandle/ExtendButton.js";
+import { TableHandle } from "./tableHandle/TableHandle.js";
+import { Toolbar } from "./toolbar/Toolbar.js";
+import { ToolbarButton } from "./toolbar/ToolbarButton.js";
+import { ToolbarSelect } from "./toolbar/ToolbarSelect.js";
+export * from "./BlockNoteTheme.js";
+export * from "./defaultThemes.js";
+
+export const components: Components = {
+ FormattingToolbar: {
+ Root: Toolbar,
+ Button: ToolbarButton,
+ Select: ToolbarSelect,
+ },
+ FilePanel: {
+ Root: Panel,
+ Button: PanelButton,
+ FileInput: PanelFileInput,
+ TabPanel: PanelTab,
+ TextInput: PanelTextInput,
+ },
+ GridSuggestionMenu: {
+ Root: GridSuggestionMenu,
+ Item: GridSuggestionMenuItem,
+ EmptyItem: GridSuggestionMenuEmptyItem,
+ Loader: GridSuggestionMenuLoader,
+ },
+ LinkToolbar: {
+ Root: Toolbar,
+ Button: ToolbarButton,
+ },
+ SideMenu: {
+ Root: SideMenu,
+ Button: SideMenuButton,
+ },
+ SuggestionMenu: {
+ Root: SuggestionMenu,
+ Item: SuggestionMenuItem,
+ EmptyItem: SuggestionMenuEmptyItem,
+ Label: SuggestionMenuLabel,
+ Loader: SuggestionMenuLoader,
+ },
+ TableHandle: {
+ Root: TableHandle,
+ ExtendButton: ExtendButton,
+ },
+ Generic: {
+ Badge: {
+ Root: Badge,
+ Group: BadgeGroup,
+ },
+ Form: {
+ Root: (props) => {props.children}
,
+ TextInput: TextInput,
+ },
+ Menu: {
+ Root: Menu,
+ Trigger: MenuTrigger,
+ Dropdown: MenuDropdown,
+ Divider: MenuDivider,
+ Label: MenuLabel,
+ Item: MenuItem,
+ },
+ Popover: {
+ Root: Popover,
+ Trigger: PopoverTrigger,
+ Content: PopoverContent,
+ },
+ Toolbar: {
+ Root: Toolbar,
+ Button: ToolbarButton,
+ },
+ },
+ Comments: {
+ Comment,
+ Editor,
+ Card,
+ CardSection,
+ },
+};
diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx
index 136e122806..f3f6a4bfc8 100644
--- a/packages/mantine/src/index.tsx
+++ b/packages/mantine/src/index.tsx
@@ -1,191 +1,2 @@
-import {
- BlockSchema,
- InlineContentSchema,
- mergeCSSClasses,
- StyleSchema,
-} from "@blocknote/core";
-import {
- BlockNoteViewProps,
- BlockNoteViewRaw,
- Components,
- ComponentsContext,
- useBlockNoteContext,
- usePrefersColorScheme,
-} from "@blocknote/react";
-import { MantineProvider } from "@mantine/core";
-import { useCallback } from "react";
-
-import {
- applyBlockNoteCSSVariablesFromTheme,
- removeBlockNoteCSSVariables,
- Theme,
-} from "./BlockNoteTheme.js";
-import { TextInput } from "./form/TextInput.js";
-import {
- Menu,
- MenuDivider,
- MenuDropdown,
- MenuItem,
- MenuLabel,
- MenuTrigger,
-} from "./menu/Menu.js";
-import { Panel } from "./panel/Panel.js";
-import { PanelButton } from "./panel/PanelButton.js";
-import { PanelFileInput } from "./panel/PanelFileInput.js";
-import { PanelTab } from "./panel/PanelTab.js";
-import { PanelTextInput } from "./panel/PanelTextInput.js";
-import { Popover, PopoverContent, PopoverTrigger } from "./popover/Popover.js";
-import { SideMenu } from "./sideMenu/SideMenu.js";
-import { SideMenuButton } from "./sideMenu/SideMenuButton.js";
-import "./style.css";
-import { GridSuggestionMenu } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.js";
-import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.js";
-import { GridSuggestionMenuItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.js";
-import { GridSuggestionMenuLoader } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.js";
-import { SuggestionMenu } from "./suggestionMenu/SuggestionMenu.js";
-import { SuggestionMenuEmptyItem } from "./suggestionMenu/SuggestionMenuEmptyItem.js";
-import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem.js";
-import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel.js";
-import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader.js";
-import { ExtendButton } from "./tableHandle/ExtendButton.js";
-import { TableHandle } from "./tableHandle/TableHandle.js";
-import { Toolbar } from "./toolbar/Toolbar.js";
-import { ToolbarButton } from "./toolbar/ToolbarButton.js";
-import { ToolbarSelect } from "./toolbar/ToolbarSelect.js";
-
-export * from "./BlockNoteTheme.js";
-export * from "./defaultThemes.js";
-
-export const components: Components = {
- FormattingToolbar: {
- Root: Toolbar,
- Button: ToolbarButton,
- Select: ToolbarSelect,
- },
- FilePanel: {
- Root: Panel,
- Button: PanelButton,
- FileInput: PanelFileInput,
- TabPanel: PanelTab,
- TextInput: PanelTextInput,
- },
- GridSuggestionMenu: {
- Root: GridSuggestionMenu,
- Item: GridSuggestionMenuItem,
- EmptyItem: GridSuggestionMenuEmptyItem,
- Loader: GridSuggestionMenuLoader,
- },
- LinkToolbar: {
- Root: Toolbar,
- Button: ToolbarButton,
- },
- SideMenu: {
- Root: SideMenu,
- Button: SideMenuButton,
- },
- SuggestionMenu: {
- Root: SuggestionMenu,
- Item: SuggestionMenuItem,
- EmptyItem: SuggestionMenuEmptyItem,
- Label: SuggestionMenuLabel,
- Loader: SuggestionMenuLoader,
- },
- TableHandle: {
- Root: TableHandle,
- ExtendButton: ExtendButton,
- },
- Generic: {
- Form: {
- Root: (props) => {props.children}
,
- TextInput: TextInput,
- },
- Menu: {
- Root: Menu,
- Trigger: MenuTrigger,
- Dropdown: MenuDropdown,
- Divider: MenuDivider,
- Label: MenuLabel,
- Item: MenuItem,
- },
- Popover: {
- Root: Popover,
- Trigger: PopoverTrigger,
- Content: PopoverContent,
- },
- },
-};
-
-const mantineTheme = {
- // Removes button press effect
- activeClassName: "",
-};
-
-export const BlockNoteView = <
- BSchema extends BlockSchema,
- ISchema extends InlineContentSchema,
- SSchema extends StyleSchema
->(
- props: Omit, "theme"> & {
- theme?:
- | "light"
- | "dark"
- | Theme
- | {
- light: Theme;
- dark: Theme;
- };
- }
-) => {
- const { className, theme, ...rest } = props;
-
- const existingContext = useBlockNoteContext();
- const systemColorScheme = usePrefersColorScheme();
- const defaultColorScheme =
- existingContext?.colorSchemePreference || systemColorScheme;
-
- const ref = useCallback(
- (node: HTMLDivElement | null) => {
- if (!node) {
- // todo: clean variables?
- return;
- }
-
- removeBlockNoteCSSVariables(node);
-
- if (typeof theme === "object") {
- if ("light" in theme && "dark" in theme) {
- applyBlockNoteCSSVariablesFromTheme(
- theme[defaultColorScheme === "dark" ? "dark" : "light"],
- node
- );
- return;
- }
-
- applyBlockNoteCSSVariablesFromTheme(theme, node);
- return;
- }
- },
- [defaultColorScheme, theme]
- );
-
- return (
-
- {/* `cssVariablesSelector` scopes Mantine CSS variables to only the editor, */}
- {/* as proposed here: https://github.com/orgs/mantinedev/discussions/5685 */}
- undefined}>
-
-
-
- );
-};
+export * from "./BlockNoteView.js";
+export * from "./components.js";
diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css
index 9fe02136fa..5692c4dbea 100644
--- a/packages/mantine/src/style.css
+++ b/packages/mantine/src/style.css
@@ -549,6 +549,7 @@
color: var(--bn-colors-tooltip-text);
padding: 4px 10px;
text-align: center;
+ white-space: pre-wrap;
}
/* Additional menu styles */
@@ -564,3 +565,116 @@
display: flex;
justify-content: space-between;
}
+
+/* TODO: Clean up */
+.bn-mantine .bn-thread {
+ background-color: var(--bn-colors-menu-background);
+ border: var(--bn-border);
+ border-radius: var(--bn-border-radius-medium);
+ box-shadow: var(--bn-shadow-medium);
+ color: var(--bn-colors-menu-text);
+ overflow: visible;
+}
+
+.bn-mantine .bn-thread .bn-grid-suggestion-menu {
+ gap: 0;
+ max-height: 300px;
+ padding: 6px;
+}
+
+.bn-mantine .bn-thread .bn-grid-suggestion-menu-item {
+ border-radius: var(--bn-border-radius-small);
+ font-size: 20px;
+ height: 32px;
+ margin: 0;
+ width: 32px;
+}
+
+.bn-mantine .bn-thread-comments {
+ border-bottom: var(--bn-border);
+}
+
+.bn-mantine .bn-comment-actions-wrapper {
+ width: 100%;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.bn-mantine .bn-badge-group {
+ display: flex;
+ gap: 4px;
+ justify-content: flex-start;
+ width: 100%;
+}
+
+.bn-mantine .bn-badge {
+ flex-grow: 0;
+}
+
+.bn-mantine .bn-badge .mantine-Chip-label {
+
+ padding: 0 8px;
+}
+
+.bn-mantine .bn-badge .mantine-Chip-label:not([data-checked="true"]) {
+ background-color: var(--bn-colors-menu-background);
+ border: var(--bn-border);
+ color: var(--bn-colors-menu-text);
+}
+
+.bn-mantine .bn-badge .mantine-Chip-label > span:not(.mantine-Chip-iconWrapper) {
+ display: inline-flex;
+ gap: 4px;
+}
+
+.bn-mantine .bn-badge .mantine-Chip-label > span:not(.mantine-Chip-iconWrapper) > span {
+ align-items: center;
+ display: inline-flex;
+ justify-content: center;
+}
+
+.bn-mantine .bn-badge .mantine-Chip-iconWrapper {
+ display: none;
+}
+
+.bn-mantine .bn-action-toolbar {
+ align-self: flex-end;
+ background-color: var(--bn-colors-menu-background);
+ border: var(--bn-border);
+ border-radius: var(--bn-border-radius-medium);
+ gap: 0;
+ padding: 2px;
+}
+
+.bn-action-toolbar .mantine-Button-root,
+.bn-action-toolbar .mantine-ActionIcon-root {
+ background-color: var(--bn-colors-menu-background);
+ border: none;
+ border-radius: var(--bn-border-radius-small);
+ color: var(--bn-colors-menu-text);
+}
+
+.bn-action-toolbar .mantine-Button-root:hover,
+.bn-action-toolbar .mantine-ActionIcon-root:hover {
+ background-color: var(--bn-colors-hovered-background);
+ border: none;
+ color: var(--bn-colors-hovered-text);
+}
+
+.bn-action-toolbar .mantine-Button-root[data-selected],
+.bn-action-toolbar .mantine-ActionIcon-root[data-selected] {
+ background-color: var(--bn-colors-selected-background);
+ border: none;
+ color: var(--bn-colors-selected-text);
+}
+
+.bn-action-toolbar .mantine-Button-root[data-disabled],
+.bn-action-toolbar .mantine-ActionIcon-root[data-disabled] {
+ background-color: var(--bn-colors-disabled-background);
+ border: none;
+ color: var(--bn-colors-disabled-text);
+}
+
+.bn-mantine .bn-action-toolbar .mantine-Menu-itemLabel {
+ font-size: 12px;
+}
\ No newline at end of file
diff --git a/packages/mantine/src/toolbar/Toolbar.tsx b/packages/mantine/src/toolbar/Toolbar.tsx
index 9ad042e850..60e12a6dbe 100644
--- a/packages/mantine/src/toolbar/Toolbar.tsx
+++ b/packages/mantine/src/toolbar/Toolbar.tsx
@@ -1,4 +1,4 @@
-import { Group as MantineGroup } from "@mantine/core";
+import { Flex } from "@mantine/core";
import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
@@ -10,7 +10,14 @@ type ToolbarProps = ComponentProps["FormattingToolbar"]["Root"] &
export const Toolbar = forwardRef(
(props, ref) => {
- const { className, children, onMouseEnter, onMouseLeave, ...rest } = props;
+ const {
+ className,
+ children,
+ onMouseEnter,
+ onMouseLeave,
+ variant,
+ ...rest
+ } = props;
assertEmpty(rest);
@@ -22,15 +29,16 @@ export const Toolbar = forwardRef(
const combinedRef = mergeRefs(ref, focusRef, trapRef);
return (
-
+ onMouseLeave={onMouseLeave}
+ gap={variant === "action-toolbar" ? 2 : undefined}>
{children}
-
+
);
}
);
diff --git a/packages/mantine/src/toolbar/ToolbarButton.tsx b/packages/mantine/src/toolbar/ToolbarButton.tsx
index bd22f04110..29dea8ee48 100644
--- a/packages/mantine/src/toolbar/ToolbarButton.tsx
+++ b/packages/mantine/src/toolbar/ToolbarButton.tsx
@@ -15,9 +15,13 @@ export const TooltipContent = (props: {
secondaryTooltip?: string;
}) => (
- {props.mainTooltip}
+
+ {props.mainTooltip}
+
{props.secondaryTooltip && (
- {props.secondaryTooltip}
+
+ {props.secondaryTooltip}
+
)}
);
@@ -40,6 +44,7 @@ export const ToolbarButton = forwardRef(
isDisabled,
onClick,
label,
+ variant,
...rest
} = props;
@@ -75,7 +80,7 @@ export const ToolbarButton = forwardRef(
mainTooltip.slice(0, 1).toLowerCase() +
mainTooltip.replace(/\s+/g, "").slice(1)
}
- size={"xs"}
+ size={variant === "compact" ? "compact-xs" : "xs"}
disabled={isDisabled || false}
ref={ref}
{...rest}>
@@ -99,7 +104,7 @@ export const ToolbarButton = forwardRef(
mainTooltip.slice(0, 1).toLowerCase() +
mainTooltip.replace(/\s+/g, "").slice(1)
}
- size={30}
+ size={variant === "compact" ? 20 : 30}
disabled={isDisabled || false}
ref={ref}
{...rest}>
diff --git a/packages/react/package.json b/packages/react/package.json
index b6ca70bc2f..0fb8a7eaba 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -6,7 +6,7 @@
"*.css"
],
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -52,7 +52,9 @@
"clean": "rimraf dist && rimraf types"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@emoji-mart/data": "^1.2.1",
+ "@emoji-mart/react": "^1.1.1",
"@floating-ui/react": "^0.26.4",
"@tiptap/core": "^2.7.1",
"@tiptap/react": "^2.7.1",
@@ -60,6 +62,7 @@
"react-icons": "^5.2.1"
},
"devDependencies": {
+ "@types/emoji-mart": "^3.0.14",
"@types/lodash.foreach": "^4.5.9",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.merge": "^4.6.9",
diff --git a/packages/react/src/blocks/PageBreakBlockContent/getPageBreakReactSlashMenuItems.tsx b/packages/react/src/blocks/PageBreakBlockContent/getPageBreakReactSlashMenuItems.tsx
new file mode 100644
index 0000000000..00fc1c274a
--- /dev/null
+++ b/packages/react/src/blocks/PageBreakBlockContent/getPageBreakReactSlashMenuItems.tsx
@@ -0,0 +1,29 @@
+import {
+ BlockNoteEditor,
+ BlockSchema,
+ getPageBreakSlashMenuItems,
+ InlineContentSchema,
+ StyleSchema,
+} from "@blocknote/core";
+import { DefaultReactSuggestionItem } from "../../components/SuggestionMenu/types.js";
+import { TbPageBreak } from "react-icons/tb";
+
+const icons = {
+ page_break: TbPageBreak,
+};
+
+export function getPageBreakReactSlashMenuItems<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ editor: BlockNoteEditor
+): (Omit & { key: "page_break" })[] {
+ return getPageBreakSlashMenuItems(editor).map((item) => {
+ const Icon = icons[item.key];
+ return {
+ ...item,
+ icon: ,
+ };
+ });
+}
diff --git a/packages/react/src/components/Comments/Comment.tsx b/packages/react/src/components/Comments/Comment.tsx
new file mode 100644
index 0000000000..0b2cb1c863
--- /dev/null
+++ b/packages/react/src/components/Comments/Comment.tsx
@@ -0,0 +1,518 @@
+"use client";
+
+import { CommentData, ThreadData, mergeCSSClasses } from "@blocknote/core";
+import data from "@emoji-mart/data";
+import Picker from "@emoji-mart/react";
+import type { EmojiData } from "emoji-mart";
+import {
+ ComponentPropsWithoutRef,
+ MouseEvent,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useState,
+} from "react";
+import {
+ RiArrowGoBackFill,
+ RiCheckFill,
+ RiDeleteBinFill,
+ RiEditFill,
+ RiEmotionFill,
+ RiMoreFill,
+} from "react-icons/ri";
+
+import { useBlockNoteContext } from "../../editor/BlockNoteContext.js";
+import { useComponentsContext } from "../../editor/ComponentsContext.js";
+import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
+import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
+import { useDictionary } from "../../i18n/dictionary.js";
+import { CommentEditor } from "./CommentEditor.js";
+import { schema } from "./schema.js";
+import { useUser } from "./useUsers.js";
+
+/**
+ * Liveblocks, but changed:
+ * - removed attachments
+ * - removed read status
+ * ...
+ */
+// const REACTIONS_TRUNCATE = 5;
+
+export interface CommentProps extends ComponentPropsWithoutRef<"div"> {
+ /**
+ * The comment to display.
+ */
+ comment: CommentData;
+
+ /**
+ * The thread id.
+ */
+ thread: ThreadData;
+
+ /**
+ * How to show or hide the actions.
+ */
+ showActions?: boolean | "hover";
+
+ /**
+ * Whether to show the resolve action.
+ */
+ showResolveAction?: boolean;
+
+ /**
+ * Whether to show the comment if it was deleted. If set to `false`, it will render deleted comments as `null`.
+ */
+ showDeleted?: boolean;
+
+ /**
+ * Whether to show reactions.
+ */
+ showReactions?: boolean;
+
+ /**
+ * @internal
+ */
+ additionalActions?: ReactNode;
+}
+
+// interface CommentReactionButtonProps
+// extends ComponentPropsWithoutRef {
+// reaction: CommentReactionData;
+// // overrides?: Partial;
+// }
+
+// interface CommentReactionProps extends ComponentPropsWithoutRef<"button"> {
+// comment: CommentData;
+// reaction: CommentReactionData;
+// // overrides?: Partial;
+// }
+
+// type CommentNonInteractiveReactionProps = Omit;
+
+// const CommentReactionButton = forwardRef<
+// HTMLButtonElement,
+// CommentReactionButtonProps
+// >(({ reaction, overrides, className, ...props }, forwardedRef) => {
+// const $ = useOverrides(overrides);
+// return (
+//
+//
+// {reaction.users.length}
+//
+// );
+// });
+
+// export const CommentReaction = forwardRef<
+// HTMLButtonElement,
+// CommentReactionProps
+// >(({ comment, reaction, overrides, disabled, ...props }, forwardedRef) => {
+// const addReaction = useAddRoomCommentReaction(comment.roomId);
+// const removeReaction = useRemoveRoomCommentReaction(comment.roomId);
+// const currentId = useCurrentUserId();
+// const isActive = useMemo(() => {
+// return reaction.users.some((users) => users.id === currentId);
+// }, [currentId, reaction]);
+// const $ = useOverrides(overrides);
+// const tooltipContent = useMemo(
+// () => (
+//
+// {$.COMMENT_REACTION_LIST(
+// (
+//
+// ))}
+// formatRemaining={$.LIST_REMAINING_USERS}
+// truncate={REACTIONS_TRUNCATE}
+// locale={$.locale}
+// />,
+// reaction.emoji,
+// reaction.users.length
+// )}
+//
+// ),
+// [$, reaction]
+// );
+
+// const stopPropagation = useCallback((event: SyntheticEvent) => {
+// event.stopPropagation();
+// }, []);
+
+// const handlePressedChange = useCallback(
+// (isPressed: boolean) => {
+// if (isPressed) {
+// addReaction({
+// threadId: comment.threadId,
+// commentId: comment.id,
+// emoji: reaction.emoji,
+// });
+// } else {
+// removeReaction({
+// threadId: comment.threadId,
+// commentId: comment.id,
+// emoji: reaction.emoji,
+// });
+// }
+// },
+// [addReaction, comment.threadId, comment.id, reaction.emoji, removeReaction]
+// );
+
+// return (
+//
+//
+//
+//
+//
+// );
+// });
+
+// export const CommentNonInteractiveReaction = forwardRef<
+// HTMLButtonElement,
+// CommentNonInteractiveReactionProps
+// >(({ reaction, overrides, ...props }, forwardedRef) => {
+// const currentId = useCurrentUserId();
+// const isActive = useMemo(() => {
+// return reaction.users.some((users) => users.id === currentId);
+// }, [currentId, reaction]);
+
+// return (
+//
+// );
+// });
+
+export const Comment = ({
+ comment,
+ thread,
+ showDeleted,
+ showActions = "hover",
+ showReactions = true,
+ showResolveAction = false,
+ className,
+ additionalActions,
+}: CommentProps) => {
+ const dict = useDictionary();
+
+ const commentEditor = useCreateBlockNote(
+ {
+ initialContent: comment.body,
+ trailingBlock: false,
+ dictionary: {
+ ...dict,
+ placeholders: {
+ ...dict.placeholders,
+ default: "Edit comment...", // TODO: only for empty doc
+ },
+ },
+ schema,
+ },
+ [comment.body]
+ );
+
+ const Components = useComponentsContext()!;
+
+ // const currentUserId = useCurrentUserId();
+ // const deleteComment = useDeleteRoomComment(comment.roomId);
+ // const editComment = useEditRoomComment(com ment.roomId);
+ // const addReaction = useAddRoomCommentReaction(comment.roomId);
+ // const removeReaction = useRemoveRoomCommentReaction(comment.roomId);
+ // const $ = useOverrides(overrides);
+ const [isEditing, setEditing] = useState(false);
+ const [isTarget, setTarget] = useState(false);
+ const [isMoreActionOpen, setMoreActionOpen] = useState(false);
+ const [isReactionActionOpen, setReactionActionOpen] = useState(false);
+
+ const editor = useBlockNoteEditor();
+
+ const handleEdit = useCallback(() => {
+ setEditing(true);
+ }, []);
+
+ const onEditCancel = useCallback(() => {
+ commentEditor.replaceBlocks(commentEditor.document, comment.body);
+ setEditing(false);
+ }, [commentEditor, comment.body]);
+
+ const onEditSubmit = useCallback(
+ async (_event: MouseEvent) => {
+ await editor.comments!.store.updateComment({
+ commentId: comment.id,
+ comment: {
+ body: commentEditor.document,
+ },
+ threadId: thread.id,
+ });
+
+ setEditing(false);
+ },
+ [comment, thread.id, commentEditor, editor.comments]
+ );
+
+ const onDelete = useCallback(() => {
+ editor.comments!.store.deleteComment({
+ commentId: comment.id,
+ threadId: thread.id,
+ });
+ }, [comment, thread.id, editor.comments]);
+
+ const onReactionSelect = useCallback(
+ (emoji: string) => {
+ editor.comments?.store.toggleReaction({
+ threadId: thread.id,
+ commentId: comment.id,
+ reaction: emoji,
+ });
+ },
+ [comment.id, editor.comments?.store, thread.id]
+ );
+
+ const onResolve = useCallback(() => {
+ editor.comments!.store.resolveThread({
+ threadId: thread.id,
+ });
+ }, [thread.id, editor.comments]);
+
+ const onReopen = useCallback(() => {
+ editor.comments!.store.unresolveThread({
+ threadId: thread.id,
+ });
+ }, [thread.id, editor.comments]);
+
+ useEffect(() => {
+ const isWindowDefined = typeof window !== "undefined";
+ if (!isWindowDefined) {
+ return;
+ }
+
+ const hash = window.location.hash;
+ const commentId = hash.slice(1);
+
+ if (commentId === comment.id) {
+ setTarget(true);
+ }
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const user = useUser(editor, comment.userId);
+
+ const blockNoteContext = useBlockNoteContext();
+
+ if (!showDeleted && !comment.body) {
+ return null;
+ }
+
+ let actions: ReactNode | undefined = undefined;
+ const canAddReaction = true; //editor.comments!.store.auth.canAddReaction(comment);
+ const canDeleteComment =
+ editor.comments!.store.auth.canDeleteComment(comment);
+ const canEditComment = editor.comments!.store.auth.canUpdateComment(comment);
+
+ const showResolveOrReopen =
+ showResolveAction &&
+ (thread.resolved
+ ? editor.comments!.store.auth.canUnresolveThread(thread)
+ : editor.comments!.store.auth.canResolveThread(thread));
+
+ if (showActions && !isEditing) {
+ actions = (
+
+ {canAddReaction && (
+
+
+
+
+
+
+
+
+ onReactionSelect(emoji.native)
+ }
+ theme={blockNoteContext?.colorSchemePreference}
+ />
+
+
+ )}
+ {showResolveOrReopen &&
+ (thread.resolved ? (
+
+
+
+ ) : (
+
+
+
+ ))}
+ {(canDeleteComment || canEditComment) && (
+
+
+
+
+
+
+
+ {canEditComment && (
+ }
+ onClick={handleEdit}>
+ Edit comment
+
+ )}
+ {canDeleteComment && (
+ }
+ onClick={onDelete}>
+ Delete comment
+
+ )}
+
+
+ )}
+
+ );
+ }
+
+ const timeString =
+ comment.createdAt.toLocaleDateString(undefined, {
+ month: "short",
+ day: "numeric",
+ }) +
+ (comment.updatedAt.getTime() !== comment.createdAt.getTime()
+ ? " (edited)"
+ : ""); // TODO: needs editedAt?
+
+ return (
+
+ {comment.body ? (
+ <>
+ (
+ <>
+ {showReactions && comment.reactions.length > 0 && (
+
+ {comment.reactions.map((reaction) => (
+ onReactionSelect(reaction.emoji)}
+ mainTooltip={"Reacted by"}
+ secondaryTooltip={`${reaction.usersIds.map(
+ (userId) => userId + "\n"
+ )}`}
+ />
+ ))}
+
+
+ }
+ />
+
+
+
+ onReactionSelect(emoji.native)
+ }
+ theme={blockNoteContext?.colorSchemePreference}
+ />
+
+
+
+ )}
+ {isEditing && (
+
+
+ Save
+
+
+ Cancel
+
+
+ )}
+ >
+ )}
+ />
+ >
+ ) : (
+ // Soft deletes
+ // TODO, test
+
+ )}
+
+ );
+};
diff --git a/packages/react/src/components/Comments/CommentEditor.tsx b/packages/react/src/components/Comments/CommentEditor.tsx
new file mode 100644
index 0000000000..e869cbae41
--- /dev/null
+++ b/packages/react/src/components/Comments/CommentEditor.tsx
@@ -0,0 +1,68 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { FC, useCallback, useState } from "react";
+import { useComponentsContext } from "../../editor/ComponentsContext.js";
+import { useEditorChange } from "../../hooks/useEditorChange.js";
+import { schema } from "./schema.js";
+
+function isDocumentEmpty(
+ editor: BlockNoteEditor<
+ typeof schema.blockSchema,
+ typeof schema.inlineContentSchema,
+ typeof schema.styleSchema
+ >
+) {
+ return (
+ editor.document.length === 0 ||
+ (editor.document.length === 1 &&
+ editor.document[0].type === "paragraph" &&
+ editor.document[0].content.length === 0)
+ );
+}
+
+export const CommentEditor = (props: {
+ editable: boolean;
+ placeholder?: string;
+ actions?: FC<{
+ isFocused: boolean;
+ isEmpty: boolean;
+ }>;
+ editor: BlockNoteEditor<
+ typeof schema.blockSchema,
+ typeof schema.inlineContentSchema,
+ typeof schema.styleSchema
+ >;
+}) => {
+ const [isFocused, setIsFocused] = useState(false);
+ const [isEmpty, setIsEmpty] = useState(isDocumentEmpty(props.editor));
+
+ const components = useComponentsContext()!;
+
+ useEditorChange(() => {
+ setIsEmpty(isDocumentEmpty(props.editor));
+ }, props.editor);
+
+ const onFocus = useCallback(() => {
+ setIsFocused(true);
+ }, []);
+
+ const onBlur = useCallback(() => {
+ setIsFocused(false);
+ }, []);
+
+ return (
+ <>
+
+ {props.actions && (
+
+ )}
+ >
+ );
+};
diff --git a/packages/react/src/components/Comments/FloatingComposer.tsx b/packages/react/src/components/Comments/FloatingComposer.tsx
new file mode 100644
index 0000000000..aa449d1d42
--- /dev/null
+++ b/packages/react/src/components/Comments/FloatingComposer.tsx
@@ -0,0 +1,59 @@
+import { mergeCSSClasses } from "@blocknote/core";
+
+import { useComponentsContext } from "../../editor/ComponentsContext.js";
+import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
+import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
+import { useDictionary } from "../../i18n/dictionary.js";
+import { CommentEditor } from "./CommentEditor.js";
+import { schema } from "./schema.js";
+
+export function FloatingComposer() {
+ const editor = useBlockNoteEditor();
+ const Components = useComponentsContext()!;
+ const dict = useDictionary();
+
+ const newCommentEditor = useCreateBlockNote({
+ trailingBlock: false,
+ dictionary: {
+ ...dict,
+ placeholders: {
+ ...dict.placeholders,
+ default: "Write a comment...", // TODO: only for empty doc
+ },
+ },
+ schema,
+ });
+
+ return (
+
+ (
+
+ {
+ await editor.comments!.createThread({
+ initialComment: {
+ body: newCommentEditor.document,
+ },
+ });
+ editor.comments!.stopPendingComment();
+ }}>
+ Save
+
+
+ )}
+ />
+
+ );
+}
diff --git a/packages/react/src/components/Comments/FloatingComposerController.tsx b/packages/react/src/components/Comments/FloatingComposerController.tsx
new file mode 100644
index 0000000000..75722dfe9e
--- /dev/null
+++ b/packages/react/src/components/Comments/FloatingComposerController.tsx
@@ -0,0 +1,76 @@
+import {
+ BlockSchema,
+ DefaultBlockSchema,
+ DefaultInlineContentSchema,
+ DefaultStyleSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "@blocknote/core";
+import { UseFloatingOptions, flip, offset } from "@floating-ui/react";
+import { ComponentProps, FC, useMemo } from "react";
+
+import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
+import { useUIElementPositioning } from "../../hooks/useUIElementPositioning.js";
+import { useUIPluginState } from "../../hooks/useUIPluginState.js";
+import { FloatingComposer } from "./FloatingComposer.js";
+
+export const FloatingComposerController = <
+ B extends BlockSchema = DefaultBlockSchema,
+ I extends InlineContentSchema = DefaultInlineContentSchema,
+ S extends StyleSchema = DefaultStyleSchema
+>(props: {
+ floatingComposer?: FC>;
+ floatingOptions?: Partial;
+}) => {
+ const editor = useBlockNoteEditor();
+
+ if (!editor.comments) {
+ throw new Error(
+ "FloatingComposerController can only be used when BlockNote editor has enabled comments"
+ );
+ }
+
+ const state = useUIPluginState(
+ editor.comments.onUpdate.bind(editor.comments)
+ );
+
+ const referencePos = useMemo(() => {
+ if (!state?.pendingComment) {
+ return null;
+ }
+
+ // TODO: update referencepos when doc changes (remote updates)
+ return editor.getSelectionBoundingBox();
+ }, [editor, state?.pendingComment]);
+
+ // TODO: review
+ const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning(
+ state?.pendingComment || false,
+ referencePos || null,
+ 5000,
+ {
+ placement: "bottom",
+ middleware: [offset(10), flip()],
+ onOpenChange: (open) => {
+ if (!open) {
+ // TODO
+ editor.comments!.stopPendingComment();
+ editor.focus();
+ }
+ },
+ ...props.floatingOptions,
+ }
+ );
+
+ if (!isMounted || !state) {
+ return null;
+ }
+
+ const Component = props.floatingComposer || FloatingComposer;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/react/src/components/Comments/FloatingThreadController.tsx b/packages/react/src/components/Comments/FloatingThreadController.tsx
new file mode 100644
index 0000000000..93bdf1ee3e
--- /dev/null
+++ b/packages/react/src/components/Comments/FloatingThreadController.tsx
@@ -0,0 +1,102 @@
+import {
+ BlockSchema,
+ DefaultBlockSchema,
+ DefaultInlineContentSchema,
+ DefaultStyleSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "@blocknote/core";
+import { UseFloatingOptions, flip, offset } from "@floating-ui/react";
+import { FC, useCallback, useEffect, useLayoutEffect } from "react";
+
+import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
+import { useUIElementPositioning } from "../../hooks/useUIElementPositioning.js";
+import { useUIPluginState } from "../../hooks/useUIPluginState.js";
+import { Thread } from "./Thread.js";
+
+/**
+ * This component is pretty close to the LiveBlocks FloatingThreads one.
+ * We have a bit of a different approach to communicating data to / from the plugin
+ */
+
+/**
+ * TODO: docs
+ */
+export const FloatingThreadController = <
+ B extends BlockSchema = DefaultBlockSchema,
+ I extends InlineContentSchema = DefaultInlineContentSchema,
+ S extends StyleSchema = DefaultStyleSchema
+>(props: {
+ filePanel?: FC; // TODO
+ floatingOptions?: Partial;
+}) => {
+ const editor = useBlockNoteEditor();
+
+ if (!editor.comments) {
+ throw new Error(
+ "FloatingComposerController can only be used when BlockNote editor has enabled comments"
+ );
+ }
+
+ const state = useUIPluginState(
+ editor.comments.onUpdate.bind(editor.comments)
+ );
+
+ // TODO: review
+ const { isMounted, ref, style, getFloatingProps, setReference } =
+ useUIElementPositioning(!!state?.selectedThreadId, null, 5000, {
+ placement: "bottom",
+ middleware: [offset(10), flip()],
+ onOpenChange: (open) => {
+ if (!open) {
+ // editor.filePanel!.closeMenu();
+ // editor.focus();
+ }
+ },
+ ...props.floatingOptions,
+ });
+
+ // TODO: could also use thread position from the state. prefer this?
+ const updateRef = useCallback(() => {
+ if (!state?.selectedThreadId) {
+ return;
+ }
+
+ const el = editor.domElement?.querySelector(
+ `[data-bn-thread-id="${state?.selectedThreadId}"]`
+ );
+ if (el) {
+ setReference(el);
+ }
+ }, [setReference, editor, state?.selectedThreadId]);
+
+ // Remote cursor updates and other edits can cause the ref to break
+ useEffect(() => {
+ if (!state?.selectedThreadId) {
+ return;
+ }
+
+ return editor.onChange(() => {
+ updateRef();
+ });
+ }, [editor, updateRef, state?.selectedThreadId]);
+
+ useLayoutEffect(updateRef, [updateRef]);
+
+ if (!isMounted || !state) {
+ return null;
+ }
+
+ if (!state.selectedThreadId) {
+ return null; // TODO
+ }
+
+ const Component = props.filePanel || Thread;
+
+ return (
+
+ );
+};
diff --git a/packages/react/src/components/Comments/Thread.tsx b/packages/react/src/components/Comments/Thread.tsx
new file mode 100644
index 0000000000..0e41c6b59c
--- /dev/null
+++ b/packages/react/src/components/Comments/Thread.tsx
@@ -0,0 +1,166 @@
+"use client";
+
+import { mergeCSSClasses } from "@blocknote/core";
+import type { ComponentPropsWithoutRef } from "react";
+import { useCallback, useMemo } from "react";
+import { useComponentsContext } from "../../editor/ComponentsContext.js";
+import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
+import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
+import { useDictionary } from "../../i18n/dictionary.js";
+import { Comment, CommentProps } from "./Comment.js";
+import { CommentEditor } from "./CommentEditor.js";
+import { schema } from "./schema.js";
+import { useThreadStore } from "./useThreadStore.js";
+import { useUsers } from "./useUsers.js";
+
+export interface ThreadProps extends ComponentPropsWithoutRef<"div"> {
+ /**
+ * The thread to display.
+ */
+ threadId: string;
+
+ /**
+ * How to show or hide the composer to reply to the thread.
+ */
+ showComposer?: boolean | "collapsed";
+
+ /**
+ * Whether to show the action to resolve the thread.
+ */
+ showResolveAction?: boolean;
+
+ /**
+ * How to show or hide the actions.
+ */
+ showActions?: CommentProps["showActions"];
+
+ /**
+ * Whether to show reactions.
+ */
+ showReactions?: CommentProps["showReactions"];
+
+ /**
+ * Whether to show deleted comments.
+ */
+ showDeletedComments?: CommentProps["showDeleted"];
+}
+
+export const Thread = ({
+ threadId,
+ showActions = "hover",
+ showDeletedComments,
+ showResolveAction = true,
+ showReactions = true,
+ className,
+ ...props
+}: ThreadProps) => {
+ // const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);
+ // const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);
+ const editor = useBlockNoteEditor();
+ const Components = useComponentsContext()!;
+ const dict = useDictionary();
+
+ const threadMap = useThreadStore(editor);
+ const thread = threadMap.get(threadId);
+
+ if (!thread) {
+ throw new Error("Thread not found");
+ }
+
+ const userIds = useMemo(() => {
+ return thread.comments.flatMap((c) => [
+ c.userId,
+ ...c.reactions.flatMap((r) => r.usersIds),
+ ]);
+ }, [thread.comments]);
+
+ // load all user data
+ useUsers(editor, userIds);
+
+ const newCommentEditor = useCreateBlockNote({
+ trailingBlock: false,
+ dictionary: {
+ ...dict,
+ placeholders: {
+ ...dict.placeholders,
+ default: "Add comment...", // TODO: only for empty doc
+ },
+ },
+ schema,
+ });
+
+ const firstCommentIndex = useMemo(() => {
+ return showDeletedComments
+ ? 0
+ : thread.comments.findIndex((comment) => comment.body);
+ }, [showDeletedComments, thread.comments]);
+
+ const onNewCommentSave = useCallback(async () => {
+ await editor.comments!.store.addComment({
+ comment: {
+ body: newCommentEditor.document,
+ },
+ threadId: thread.id,
+ });
+
+ // reset editor
+ newCommentEditor.removeBlocks(newCommentEditor.document);
+ }, [editor.comments, newCommentEditor, thread.id]);
+
+ const showComposer = editor.comments!.store.auth.canAddComment(thread);
+
+ return (
+
+
+ {thread.comments.map((comment, index) => {
+ const isFirstComment = index === firstCommentIndex;
+
+ return (
+
+ );
+ })}
+
+ {showComposer && (
+
+ {
+ if (!isFocused && isEmpty) {
+ return null;
+ }
+
+ return (
+
+
+ Save
+
+
+ );
+ }}
+ />
+
+ )}
+
+ );
+};
diff --git a/packages/react/src/components/Comments/schema.ts b/packages/react/src/components/Comments/schema.ts
new file mode 100644
index 0000000000..c9078380d7
--- /dev/null
+++ b/packages/react/src/components/Comments/schema.ts
@@ -0,0 +1,8 @@
+import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
+
+// TODO: disable props on paragraph
+export const schema = BlockNoteSchema.create({
+ blockSpecs: {
+ paragraph: defaultBlockSpecs.paragraph,
+ },
+});
diff --git a/packages/react/src/components/Comments/useThreadStore.ts b/packages/react/src/components/Comments/useThreadStore.ts
new file mode 100644
index 0000000000..1c37a25345
--- /dev/null
+++ b/packages/react/src/components/Comments/useThreadStore.ts
@@ -0,0 +1,28 @@
+import { BlockNoteEditor, ThreadData } from "@blocknote/core";
+import { useCallback, useRef, useSyncExternalStore } from "react";
+
+export function useThreadStore(editor: BlockNoteEditor) {
+ const store = editor.comments!.store;
+
+ // this ref works around this error:
+ // https://react.dev/reference/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
+ // however, might not be a good practice to work around it this way
+ const threadsRef = useRef>();
+
+ if (!threadsRef.current) {
+ threadsRef.current = store.getThreads();
+ }
+
+ const subscribe = useCallback(
+ (cb: () => void) => {
+ return store.subscribe((threads) => {
+ // update ref when changed
+ threadsRef.current = threads;
+ cb();
+ });
+ },
+ [store]
+ );
+
+ return useSyncExternalStore(subscribe, () => threadsRef.current!);
+}
diff --git a/packages/react/src/components/Comments/useUsers.ts b/packages/react/src/components/Comments/useUsers.ts
new file mode 100644
index 0000000000..62279d9810
--- /dev/null
+++ b/packages/react/src/components/Comments/useUsers.ts
@@ -0,0 +1,52 @@
+import { BlockNoteEditor, User } from "@blocknote/core";
+import { useCallback, useRef, useSyncExternalStore } from "react";
+
+export function useUser(
+ editor: BlockNoteEditor,
+ userId: string
+) {
+ return useUsers(editor, [userId]).get(userId);
+}
+
+export function useUsers(
+ editor: BlockNoteEditor,
+ userIds: string[]
+) {
+ const store = editor.comments!.userStore;
+
+ // this ref works around this error:
+ // https://react.dev/reference/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
+ // however, might not be a good practice to work around it this way
+ const usersRef = useRef>();
+
+ const getSnapshot = useCallback(() => {
+ const map = new Map();
+ for (const id of userIds) {
+ const user = store.getUser(id);
+ if (user) {
+ map.set(id, user);
+ }
+ }
+ return map;
+ }, [store, userIds]);
+
+ if (!usersRef.current) {
+ usersRef.current = getSnapshot();
+ }
+
+ // note: this is inefficient as it will trigger a re-render even if other users (not in userIds) are updated
+ const subscribe = useCallback(
+ (cb: () => void) => {
+ const ret = store.subscribe((_users) => {
+ // update ref when changed
+ usersRef.current = getSnapshot();
+ cb();
+ });
+ store.loadUsers(userIds);
+ return ret;
+ },
+ [store, getSnapshot, userIds]
+ );
+
+ return useSyncExternalStore(subscribe, () => usersRef.current!);
+}
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
index 50c8a6911e..bd00a988f7 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
@@ -17,13 +17,14 @@ export const AddCommentButton = () => {
>();
const onClick = useCallback(() => {
- (editor._tiptapEditor as any).chain().focus().addPendingComment().run();
+ editor.comments?.startPendingComment();
+ editor.formattingToolbar.closeMenu();
}, [editor]);
if (
// We manually check if a comment extension (like liveblocks) is installed
// By adding default support for this, the user doesn't need to customize the formatting toolbar
- !(editor._tiptapEditor.commands as any)["addPendingComment"] ||
+ !editor.comments ||
!editor.isEditable
) {
return null;
diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts
index b7d0d2bb83..8a9bb1019c 100644
--- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts
+++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts
@@ -35,6 +35,7 @@ export function useSuggestionMenuKeyboardNavigation- (
if (event.key === "Enter" && !event.isComposing) {
event.preventDefault();
+ event.stopPropagation();
if (items.length) {
onItemClick?.(items[selectedIndex]);
diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx
index 757dac7989..606389acab 100644
--- a/packages/react/src/editor/BlockNoteDefaultUI.tsx
+++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx
@@ -1,3 +1,5 @@
+import { FloatingComposerController } from "../components/Comments/FloatingComposerController.js";
+import { FloatingThreadController } from "../components/Comments/FloatingThreadController.js";
import { FilePanelController } from "../components/FilePanel/FilePanelController.js";
import { FormattingToolbarController } from "../components/FormattingToolbar/FormattingToolbarController.js";
import { LinkToolbarController } from "../components/LinkToolbar/LinkToolbarController.js";
@@ -15,6 +17,7 @@ export type BlockNoteDefaultUIProps = {
filePanel?: boolean;
tableHandles?: boolean;
emojiPicker?: boolean;
+ comments?: boolean;
};
export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) {
@@ -45,6 +48,12 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) {
{editor.tableHandles && props.tableHandles !== false && (
)}
+ {editor.comments && props.comments !== false && (
+ <>
+
+
+ >
+ )}
>
);
}
diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx
index 27a5fc68f8..1c23ec8c8f 100644
--- a/packages/react/src/editor/BlockNoteView.tsx
+++ b/packages/react/src/editor/BlockNoteView.tsx
@@ -157,8 +157,14 @@ function BlockNoteViewComponent<
const mount = useCallback(
(element: HTMLElement | null) => {
editor.mount(element, portalManager);
+
+ // Since we mount the editor ourselves, we also have to manually
+ // autofocus it on mount.
+ if (rest.autoFocus) {
+ element?.focus();
+ }
},
- [editor, portalManager]
+ [editor, portalManager, rest.autoFocus]
);
return (
diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx
index 123264a199..6d5194ab23 100644
--- a/packages/react/src/editor/ComponentsContext.tsx
+++ b/packages/react/src/editor/ComponentsContext.tsx
@@ -9,27 +9,35 @@ import {
useContext,
} from "react";
+import { BlockNoteEditor, User } from "@blocknote/core";
import { DefaultReactGridSuggestionItem } from "../components/SuggestionMenu/GridSuggestionMenu/types.js";
import { DefaultReactSuggestionItem } from "../components/SuggestionMenu/types.js";
+type ToolbarRootType = {
+ className?: string;
+ children?: ReactNode;
+ onMouseEnter?: () => void;
+ onMouseLeave?: () => void;
+ variant?: "default" | "action-toolbar";
+};
+
+type ToolbarButtonType = {
+ className?: string;
+ mainTooltip: string;
+ secondaryTooltip?: string;
+ icon?: ReactNode;
+ onClick?: (e: MouseEvent) => void;
+ isSelected?: boolean;
+ isDisabled?: boolean;
+ variant?: "default" | "compact";
+} & (
+ | { children: ReactNode; label?: string }
+ | { children?: undefined; label: string }
+);
export type ComponentProps = {
FormattingToolbar: {
- Root: {
- className?: string;
- children?: ReactNode;
- };
- Button: {
- className?: string;
- mainTooltip: string;
- secondaryTooltip?: string;
- icon?: ReactNode;
- onClick?: (e: MouseEvent) => void;
- isSelected?: boolean;
- isDisabled?: boolean;
- } & (
- | { children: ReactNode; label?: string }
- | { children?: undefined; label: string }
- );
+ Root: ToolbarRootType;
+ Button: ToolbarButtonType;
Select: {
className?: string;
items: {
@@ -81,24 +89,8 @@ export type ComponentProps = {
};
};
LinkToolbar: {
- Root: {
- className?: string;
- children?: ReactNode;
- onMouseEnter?: () => void;
- onMouseLeave?: () => void;
- };
- Button: {
- className?: string;
- mainTooltip: string;
- secondaryTooltip?: string;
- icon?: ReactNode;
- onClick?: (e: MouseEvent) => void;
- isSelected?: boolean;
- isDisabled?: boolean;
- } & (
- | { children: ReactNode; label?: string }
- | { children?: undefined; label: string }
- );
+ Root: ToolbarRootType;
+ Button: ToolbarButtonType;
};
SideMenu: {
Root: {
@@ -192,6 +184,21 @@ export type ComponentProps = {
};
// TODO: We should try to make everything as generic as we can
Generic: {
+ Badge: {
+ Root: {
+ className?: string;
+ text: string;
+ icon?: ReactNode;
+ isSelected?: boolean;
+ mainTooltip?: string;
+ secondaryTooltip?: string;
+ onClick?: () => void;
+ };
+ Group: {
+ className?: string;
+ children: ReactNode;
+ };
+ };
Form: {
Root: {
children?: ReactNode;
@@ -257,6 +264,35 @@ export type ComponentProps = {
children?: ReactNode;
};
};
+ Toolbar: {
+ Root: ToolbarRootType;
+ Button: ToolbarButtonType;
+ };
+ };
+ Comments: {
+ Card: {
+ className?: string;
+ children?: ReactNode;
+ };
+ CardSection: {
+ className?: string;
+ children?: ReactNode;
+ };
+ Editor: {
+ className?: string;
+ editable: boolean;
+ editor: BlockNoteEditor;
+ onFocus?: () => void;
+ onBlur?: () => void;
+ };
+ Comment: {
+ className?: string;
+ children?: ReactNode;
+ authorInfo: "loading" | User;
+ timeString: string;
+ actions?: ReactNode;
+ showActions?: boolean | "hover";
+ };
};
};
diff --git a/packages/react/src/hooks/useUIElementPositioning.ts b/packages/react/src/hooks/useUIElementPositioning.ts
index 328e481eb9..926447b149 100644
--- a/packages/react/src/hooks/useUIElementPositioning.ts
+++ b/packages/react/src/hooks/useUIElementPositioning.ts
@@ -43,6 +43,7 @@ export function useUIElementPositioning(
return {
isMounted,
ref: refs.setFloating,
+ setReference: refs.setReference,
style: {
display: "flex",
...styles,
@@ -56,6 +57,7 @@ export function useUIElementPositioning(
floatingStyles,
isMounted,
refs.setFloating,
+ refs.setReference,
styles,
zIndex,
getFloatingProps,
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 579f6bad1e..dd8215d700 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -15,6 +15,7 @@ export * from "./blocks/FileBlockContent/helpers/toExternalHTML/FigureWithCaptio
export * from "./blocks/FileBlockContent/helpers/toExternalHTML/LinkWithCaption.js";
export * from "./blocks/FileBlockContent/useResolveUrl.js";
export * from "./blocks/ImageBlockContent/ImageBlockContent.js";
+export * from "./blocks/PageBreakBlockContent/getPageBreakReactSlashMenuItems.js";
export * from "./blocks/VideoBlockContent/VideoBlockContent.js";
export * from "./components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.js";
diff --git a/packages/server-util/package.json b/packages/server-util/package.json
index 6644da658f..04f682a3fc 100644
--- a/packages/server-util/package.json
+++ b/packages/server-util/package.json
@@ -6,7 +6,7 @@
"*.css"
],
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -50,8 +50,8 @@
"test-watch": "vitest watch"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@tiptap/core": "^2.7.1",
"@tiptap/pm": "^2.7.1",
"jsdom": "^25.0.1",
diff --git a/packages/server-util/vite.config.ts b/packages/server-util/vite.config.ts
index 940749ef1c..c134c61e54 100644
--- a/packages/server-util/vite.config.ts
+++ b/packages/server-util/vite.config.ts
@@ -4,7 +4,11 @@ import { defineConfig } from "vite";
import pkg from "./package.json";
// import eslintPlugin from "vite-plugin-eslint";
-const deps = Object.keys(pkg.dependencies);
+const deps = Object.keys({
+ ...pkg.dependencies,
+ ...pkg.peerDependencies,
+ ...pkg.devDependencies,
+});
// https://vitejs.dev/config/
export default defineConfig((conf) => ({
@@ -37,7 +41,16 @@ export default defineConfig((conf) => ({
if (deps.includes(source)) {
return true;
}
- return source.startsWith("prosemirror-");
+
+ if (source === "react/jsx-runtime") {
+ return true;
+ }
+
+ if (source.startsWith("prosemirror-")) {
+ return true;
+ }
+
+ return false;
},
output: {
// Provide global variables to use in the UMD build
diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json
index 99bb90d01f..da838ba4cd 100644
--- a/packages/shadcn/package.json
+++ b/packages/shadcn/package.json
@@ -6,7 +6,7 @@
"*.css"
],
"license": "MPL-2.0",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -50,8 +50,8 @@
"clean": "rimraf dist && rimraf types"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@hookform/resolvers": "^3.6.0",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
diff --git a/packages/xl-docx-exporter/package.json b/packages/xl-docx-exporter/package.json
index 6a088a1dbd..e42ef0c05f 100644
--- a/packages/xl-docx-exporter/package.json
+++ b/packages/xl-docx-exporter/package.json
@@ -3,7 +3,7 @@
"homepage": "https://github.com/TypeCellOS/BlockNote",
"private": false,
"license": "AGPL-3.0 OR PROPRIETARY",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -50,7 +50,7 @@
"email": "email dev"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
"buffer": "^6.0.3",
"docx": "^9.0.2",
"sharp": "^0.33.5"
@@ -67,6 +67,10 @@
"vitest": "^2.0.3",
"xml-formatter": "^3.6.3"
},
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || >= 19.0.0-rc",
+ "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc"
+ },
"eslintConfig": {
"extends": [
"../../.eslintrc.js"
diff --git a/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml b/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml
index 252040fe31..ccd1ff2147 100644
--- a/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml
+++ b/packages/xl-docx-exporter/src/docx/__snapshots__/basic/document.xml
@@ -113,12 +113,8 @@
-
-
-
- Code Block
-Line 2
+
@@ -675,6 +671,23 @@ Line 2
+
+
+
+
+
+
+ const helloWorld = (message) => {
+
+
+
+ console.log("Hello World", message);
+
+
+
+ };
+
+
diff --git a/packages/xl-docx-exporter/src/docx/__snapshots__/basic/styles.xml b/packages/xl-docx-exporter/src/docx/__snapshots__/basic/styles.xml
index 2b3704b69a..d23b5bb2ff 100644
--- a/packages/xl-docx-exporter/src/docx/__snapshots__/basic/styles.xml
+++ b/packages/xl-docx-exporter/src/docx/__snapshots__/basic/styles.xml
@@ -953,8 +953,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts b/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts
index 43bfba9781..1b457f3c35 100644
--- a/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts
+++ b/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts
@@ -3,6 +3,8 @@ import {
COLORS_DEFAULT,
DefaultBlockSchema,
DefaultProps,
+ pageBreakSchema,
+ StyledText,
UnreachableCaseError,
} from "@blocknote/core";
import {
@@ -11,6 +13,7 @@ import {
ExternalHyperlink,
IParagraphOptions,
ImageRun,
+ PageBreak,
Paragraph,
ParagraphChild,
ShadingType,
@@ -55,7 +58,7 @@ function blockPropsToStyles(
};
}
export const docxBlockMappingForDefaultSchema: BlockMapping<
- DefaultBlockSchema,
+ DefaultBlockSchema & typeof pageBreakSchema.blockSchema,
any,
any,
| Promise
@@ -131,17 +134,29 @@ export const docxBlockMappingForDefaultSchema: BlockMapping<
...caption(block.props, exporter),
];
},
- // TODO
- codeBlock: (block, exporter) => {
+ codeBlock: (block) => {
+ const textContent = (block.content as StyledText[])[0]?.text || "";
+
return new Paragraph({
- // ...blockPropsToStyles(block.props, exporter.options.colors),
style: "Codeblock",
- children: exporter.transformInlineContent(block.content),
- // children: [
- // new TextRun({
- // text: block..type + " not implemented",
- // }),
- // ],
+ shading: {
+ type: ShadingType.SOLID,
+ fill: "161616",
+ color: "161616",
+ },
+ children: [
+ ...textContent.split("\n").map((line, index) => {
+ return new TextRun({
+ text: line,
+ break: index > 0 ? 1 : 0,
+ });
+ }),
+ ],
+ });
+ },
+ pageBreak: () => {
+ return new Paragraph({
+ children: [new PageBreak()],
});
},
image: async (block, exporter) => {
diff --git a/packages/xl-docx-exporter/src/docx/defaultSchema/styles.ts b/packages/xl-docx-exporter/src/docx/defaultSchema/styles.ts
index 313a0b244a..1d56e25aaa 100644
--- a/packages/xl-docx-exporter/src/docx/defaultSchema/styles.ts
+++ b/packages/xl-docx-exporter/src/docx/defaultSchema/styles.ts
@@ -67,7 +67,7 @@ export const docxStyleMappingForDefaultSchema: StyleMapping<
return {};
}
return {
- font: "Courier New",
+ font: "GeistMono",
};
},
};
diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts
index 42f7449006..ec2d3d9eef 100644
--- a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts
+++ b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts
@@ -1,4 +1,4 @@
-import { BlockNoteSchema } from "@blocknote/core";
+import { BlockNoteSchema, defaultBlockSpecs, PageBreak } from "@blocknote/core";
import { testDocument } from "@shared/testDocument.js";
import AdmZip from "adm-zip";
import { Packer, Paragraph, TextRun } from "docx";
@@ -10,7 +10,9 @@ import { DOCXExporter } from "./docxExporter.js";
describe("exporter", () => {
it("should export a document", { timeout: 10000 }, async () => {
const exporter = new DOCXExporter(
- BlockNoteSchema.create(),
+ BlockNoteSchema.create({
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
+ }),
docxDefaultSchemaMappings
);
const doc = await exporter.toDocxJsDocument(testDocument);
@@ -33,7 +35,9 @@ describe("exporter", () => {
{ timeout: 10000 },
async () => {
const exporter = new DOCXExporter(
- BlockNoteSchema.create(),
+ BlockNoteSchema.create({
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
+ }),
docxDefaultSchemaMappings
);
diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.ts b/packages/xl-docx-exporter/src/docx/docxExporter.ts
index 2be6106591..7b0c8cc084 100644
--- a/packages/xl-docx-exporter/src/docx/docxExporter.ts
+++ b/packages/xl-docx-exporter/src/docx/docxExporter.ts
@@ -142,17 +142,35 @@ export class DOCXExporter<
// Unfortunately, loading the variable font doesn't work
// "./src/fonts/Inter-VariableFont_opsz,wght.ttf",
- let font = await loadFileBuffer(
+ let interFont = await loadFileBuffer(
await import("@shared/assets/fonts/inter/Inter_18pt-Regular.ttf")
);
+ let geistMonoFont = await loadFileBuffer(
+ await import("@shared/assets/fonts/GeistMono-Regular.ttf")
+ );
- if (font instanceof ArrayBuffer) {
- // conversionw with Polyfill needed because docxjs requires Buffer
+ if (
+ interFont instanceof ArrayBuffer ||
+ geistMonoFont instanceof Uint8Array
+ ) {
+ // conversion with Polyfill needed because docxjs requires Buffer
const Buffer = (await import("buffer")).Buffer;
- font = Buffer.from(font);
+
+ if (interFont instanceof ArrayBuffer) {
+ interFont = Buffer.from(interFont);
+ }
+ if (geistMonoFont instanceof ArrayBuffer) {
+ geistMonoFont = Buffer.from(geistMonoFont);
+ }
}
- return [{ name: "Inter", data: font as Buffer }];
+ return [
+ { name: "Inter", data: interFont as Buffer },
+ {
+ name: "GeistMono",
+ data: geistMonoFont as Buffer,
+ },
+ ];
}
protected async createDefaultDocumentOptions(): Promise {
diff --git a/packages/xl-docx-exporter/src/docx/template/word/styles.xml b/packages/xl-docx-exporter/src/docx/template/word/styles.xml
index e3489ab35e..8e7d713135 100644
--- a/packages/xl-docx-exporter/src/docx/template/word/styles.xml
+++ b/packages/xl-docx-exporter/src/docx/template/word/styles.xml
@@ -983,8 +983,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/packages/xl-docx-exporter/vite.config.ts b/packages/xl-docx-exporter/vite.config.ts
index b17d4637b1..bf56284285 100644
--- a/packages/xl-docx-exporter/vite.config.ts
+++ b/packages/xl-docx-exporter/vite.config.ts
@@ -4,7 +4,11 @@ import { defineConfig } from "vite";
import pkg from "./package.json";
// import eslintPlugin from "vite-plugin-eslint";
-const deps = Object.keys(pkg.dependencies);
+const deps = Object.keys({
+ ...pkg.dependencies,
+ ...pkg.peerDependencies,
+ ...pkg.devDependencies,
+});
// https://vitejs.dev/config/
export default defineConfig((conf) => ({
@@ -46,7 +50,16 @@ export default defineConfig((conf) => ({
if (deps.includes(source)) {
return true;
}
- return source.startsWith("prosemirror-");
+
+ if (source === "react/jsx-runtime") {
+ return true;
+ }
+
+ if (source.startsWith("prosemirror-")) {
+ return true;
+ }
+
+ return false;
},
output: {
// Provide global variables to use in the UMD build
diff --git a/packages/xl-multi-column/package.json b/packages/xl-multi-column/package.json
index f0362ce271..13e21978eb 100644
--- a/packages/xl-multi-column/package.json
+++ b/packages/xl-multi-column/package.json
@@ -3,7 +3,7 @@
"homepage": "https://github.com/TypeCellOS/BlockNote",
"private": false,
"license": "AGPL-3.0 OR PROPRIETARY",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -45,8 +45,8 @@
"clean": "rimraf dist && rimraf types"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@tiptap/core": "^2.7.1",
"prosemirror-model": "^1.23.0",
"prosemirror-state": "^1.4.3",
@@ -67,6 +67,10 @@
"vite-plugin-eslint": "^1.8.1",
"vitest": "^2.0.3"
},
+ "peerDependencies": {
+ "react": "^18.0 || ^19.0 || >= 19.0.0-rc",
+ "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc"
+ },
"eslintConfig": {
"extends": [
"../../.eslintrc.js"
diff --git a/packages/xl-multi-column/vite.config.ts b/packages/xl-multi-column/vite.config.ts
index 4aa18f6711..d82b5b09a3 100644
--- a/packages/xl-multi-column/vite.config.ts
+++ b/packages/xl-multi-column/vite.config.ts
@@ -4,7 +4,11 @@ import { defineConfig } from "vite";
import pkg from "./package.json";
// import eslintPlugin from "vite-plugin-eslint";
-const deps = Object.keys(pkg.dependencies);
+const deps = Object.keys({
+ ...pkg.dependencies,
+ ...pkg.peerDependencies,
+ ...pkg.devDependencies,
+});
// https://vitejs.dev/config/
export default defineConfig((conf) => ({
@@ -36,7 +40,16 @@ export default defineConfig((conf) => ({
if (deps.includes(source)) {
return true;
}
- return source.startsWith("prosemirror-");
+
+ if (source === "react/jsx-runtime") {
+ return true;
+ }
+
+ if (source.startsWith("prosemirror-")) {
+ return true;
+ }
+
+ return false;
},
output: {
// Provide global variables to use in the UMD build
diff --git a/packages/xl-pdf-exporter/package.json b/packages/xl-pdf-exporter/package.json
index 4a50afbfea..426ab2b500 100644
--- a/packages/xl-pdf-exporter/package.json
+++ b/packages/xl-pdf-exporter/package.json
@@ -3,7 +3,7 @@
"homepage": "https://github.com/TypeCellOS/BlockNote",
"private": false,
"license": "AGPL-3.0 OR PROPRIETARY",
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -49,8 +49,8 @@
"email": "email dev"
},
"dependencies": {
- "@blocknote/core": "^0.22.0",
- "@blocknote/react": "^0.22.0",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
"@react-pdf/renderer": "^4.0.0",
"buffer": "^6.0.3",
"docx": "^9.0.2"
@@ -65,6 +65,7 @@
"jest-image-snapshot": "^6.4.0",
"pdf-to-img": "^4.2.0",
"prettier": "^2.7.1",
+ "react-element-to-jsx-string": "^15.0.0",
"rollup-plugin-webpack-stats": "^0.2.2",
"typescript": "^5.0.4",
"vite": "^5.3.4",
diff --git a/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx b/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx
index e7158793d3..74a79c1821 100644
--- a/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx
+++ b/packages/xl-pdf-exporter/src/pdf/__snapshots__/example.jsx
@@ -1,592 +1,935 @@
-
-
-
-
-
-
-
- Welcome to this
-
-
- demo 🙌!
-
-
-
-
-
+
+
+
+
+
-
-
- Hello World nested
-
-
-
-
+
-
-
-
- Hello World double nested
-
-
-
-
-
-
-
-
- This paragraph has a background color
-
-
-
-
-
-
- Paragraph
-
-
-
-
-
+
+
+
+
+
-
- Heading
-
-
-
-
-
+
+ Hello World nested
+
+
+
+
-
- Heading right
-
-
-
-
-
-
- justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
- codeBlock not implemented
-
-
-
+
+
+
+ Hello World double nested
+
+
+
+
+
+
+
+
+
+
+
+
+ This paragraph has a background color
+
+
+
+
+
+
+
+
+ Paragraph
+
+
+
+
+
+
+
-
-
-
- •
-
-
-
-
- Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
+ Heading
+
+
+
+
+
+
+
-
+ Heading right
+
+
+
+
+
+
+
+
+ justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+
+
+
+
+ Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+
-
-
-
- •
-
-
-
-
+
+
+
Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- •
-
-
-
-
+
+
+
Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- 1.
-
-
-
-
+
+
+
Numbered List Item 1
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- 2.
-
-
-
-
+
+
+
Numbered List Item 2
-
-
-
-
-
+
+
+
+
-
-
-
-
- 1.
-
-
-
-
- Numbered List Item Nested 1
-
-
-
-
-
-
-
-
- 2.
-
-
-
-
- Numbered List Item Nested 2
-
-
-
-
-
-
-
-
- 3.
-
-
-
-
- Numbered List Item Nested funky right
-
-
-
-
-
-
-
-
- 4.
-
-
-
-
- Numbered List Item Nested funky center
-
-
-
-
-
-
-
-
-
-
- 1.
-
-
-
-
- Numbered List Item
-
-
-
-
-
-
-
-
+
-
-
-
-
-
- Check List Item
-
-
-
-
-
-
-
-
-
- Wide Cell
-
-
-
-
- Table Cell
-
-
-
-
- Table Cell
-
-
-
-
-
-
- Wide Cell
-
-
-
-
- Table Cell
-
-
-
-
- Table Cell
-
-
-
-
-
-
- Wide Cell
-
-
-
-
- Table Cell
-
-
-
-
- Table Cell
-
-
-
-
-
-
-
-
-
+
+
+ Numbered List Item Nested 1
+
+
+
+
+
+
+
-
-
-
-
- Open file
-
-
-
-
-
-
-
-
-
- From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Numbered List Item Nested 2
+
+
+
+
+
+
+
-
-
-
-
- Open video file
-
-
-
-
- From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm
-
-
-
-
-
-
-
+
+
+ Numbered List Item Nested funky right
+
+
+
+
+
+
+
-
-
-
-
- Open audio file
-
-
-
-
+
+
+ Numbered List Item Nested funky center
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Numbered List Item
+
+
+
+
+
+
+
+ }>
+
+
+ Check List Item
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3
-
-
-
-
-
-
-
-
-
+
+
+
+ Open file
+
+
+
+
+
+
+
+
+
+
+
+ From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- audio.mp3
-
-
-
-
+
+
+ Open video file
+
+
+
+
+ From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm
+
+
+
+
+
+
+
+
+
- Audio file caption
-
-
-
-
-
-
+
+
+
+ Open audio file
+
+
+
+
+ From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Inline Content:
-
-
-
-
+
+
+
+ audio.mp3
+
+
+
+
+ Audio file caption
+
+
+
+
+
+
+
+
+ Inline Content:
+
+
+
+
+
+
+
+
+ Styled Text
+
+
+ {' '}
+
+
+
+ Link
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- Styled Text
-
-
-
-
-
-
- Link
-
-
-
-
-
-
-
-
-
- Table Cell 1
-
-
-
-
- Table Cell 2
-
-
-
-
- Table Cell 3
-
-
-
-
-
-
- Table Cell 4
-
-
-
-
- Table Cell Bold 5
-
-
-
-
- Table Cell 6
-
-
-
-
-
-
- Table Cell 7
-
-
-
-
- Table Cell 8
-
-
-
-
- Table Cell 9
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+ {`const helloWorld = (message) => {`}
+
+
+ console.log("Hello World", message);
+
+
+ {`};`}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx b/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx
index fc93b8c7d0..3c6a68a2f7 100644
--- a/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx
+++ b/packages/xl-pdf-exporter/src/pdf/__snapshots__/exampleWithHeaderAndFooter.jsx
@@ -1,602 +1,960 @@
-
-
-
+
+
-
-
- Header
-
-
-
-
-
-
- Welcome to this
-
-
- demo 🙌!
-
-
-
-
-
+ Header
+
+
+
+
+
+
-
-
- Hello World nested
-
-
-
-
+
-
-
-
- Hello World double nested
-
-
-
-
-
-
-
-
- This paragraph has a background color
-
-
-
-
-
-
- Paragraph
-
-
-
-
-
+
+
+
+
+
-
- Heading
-
-
-
-
-
+
+ Hello World nested
+
+
+
+
-
- Heading right
-
-
-
-
-
-
- justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
- codeBlock not implemented
-
-
-
+
+
+
+ Hello World double nested
+
+
+
+
+
+
+
+
+
+
+
+
+ This paragraph has a background color
+
+
+
+
+
+
+
+
+ Paragraph
+
+
+
+
+
+
+
-
-
-
- •
-
-
-
-
- Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
+ Heading
+
+
+
+
+
+
+
-
+ Heading right
+
+
+
+
+
+
+
+
+ justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+
+
+
+
+ Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+
-
-
-
- •
-
-
-
-
+
+
+
Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- •
-
-
-
-
+
+
+
Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- 1.
-
-
-
-
+
+
+
Numbered List Item 1
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- 2.
-
-
-
-
+
+
+
Numbered List Item 2
-
-
-
-
-
+
+
+
+
-
-
-
-
- 1.
-
-
-
-
- Numbered List Item Nested 1
-
-
-
-
-
-
-
-
- 2.
-
-
-
-
- Numbered List Item Nested 2
-
-
-
-
-
-
-
-
- 3.
-
-
-
-
- Numbered List Item Nested funky right
-
-
-
-
-
-
-
-
- 4.
-
-
-
-
- Numbered List Item Nested funky center
-
-
-
-
-
-
-
-
-
-
- 1.
-
-
-
-
- Numbered List Item
-
-
-
-
-
-
-
-
+
-
-
-
-
-
- Check List Item
-
-
-
-
-
-
-
-
-
- Wide Cell
-
-
-
-
- Table Cell
-
-
-
-
- Table Cell
-
-
-
-
-
-
- Wide Cell
-
-
-
-
- Table Cell
-
-
-
-
- Table Cell
-
-
-
-
-
-
- Wide Cell
-
-
-
-
- Table Cell
-
-
-
-
- Table Cell
-
-
-
-
-
-
-
-
-
+
+
+ Numbered List Item Nested 1
+
+
+
+
+
+
+
-
-
-
-
- Open file
-
-
-
-
-
-
-
-
-
- From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Numbered List Item Nested 2
+
+
+
+
+
+
+
-
-
-
-
- Open video file
-
-
-
-
- From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm
-
-
-
-
-
-
-
+
+
+ Numbered List Item Nested funky right
+
+
+
+
+
+
+
-
-
-
-
- Open audio file
-
-
-
-
+
+
+ Numbered List Item Nested funky center
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Numbered List Item
+
+
+
+
+
+
+
+ }>
+
+
+ Check List Item
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3
-
-
-
-
-
-
-
-
-
+
+
+
+ Open file
+
+
+
+
+
+
+
+
+
+
+
+ From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- audio.mp3
-
-
-
-
+
+
+ Open video file
+
+
+
+
+ From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm
+
+
+
+
+
+
+
+
+
- Audio file caption
-
-
-
-
-
-
+
+
+
+ Open audio file
+
+
+
+
+ From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Inline Content:
-
-
-
-
+
+
+
+ audio.mp3
+
+
+
+
+ Audio file caption
+
+
+
+
+
+
+
+
+ Inline Content:
+
+
+
+
+
+
+
+
+ Styled Text
+
+
+ {' '}
+
+
+
+ Link
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- Styled Text
-
-
-
-
-
-
- Link
-
-
-
-
-
-
-
-
-
- Table Cell 1
-
-
-
-
- Table Cell 2
-
-
-
-
- Table Cell 3
-
-
-
-
-
-
- Table Cell 4
-
-
-
-
- Table Cell Bold 5
-
-
-
-
- Table Cell 6
-
-
-
-
-
-
- Table Cell 7
-
-
-
-
- Table Cell 8
-
-
-
-
- Table Cell 9
-
-
-
-
-
-
-
-
- Footer
-
-
-
-
-
\ No newline at end of file
+
+ {`const helloWorld = (message) => {`}
+
+
+ console.log("Hello World", message);
+
+
+ {`};`}
+
+
+
+
+
+
+ Footer
+
+
+
+
\ No newline at end of file
diff --git a/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx b/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
index 400422d28d..9e75c2d273 100644
--- a/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
+++ b/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
@@ -2,6 +2,8 @@ import {
BlockMapping,
DefaultBlockSchema,
DefaultProps,
+ pageBreakSchema,
+ StyledText,
} from "@blocknote/core";
import { Image, Link, Path, Svg, Text, View } from "@react-pdf/renderer";
import {
@@ -16,7 +18,7 @@ const PIXELS_PER_POINT = 0.75;
const FONT_SIZE = 16;
export const pdfBlockMappingForDefaultSchema: BlockMapping<
- DefaultBlockSchema,
+ DefaultBlockSchema & typeof pageBreakSchema.blockSchema,
any,
any,
React.ReactElement,
@@ -69,7 +71,38 @@ export const pdfBlockMappingForDefaultSchema: BlockMapping<
);
},
codeBlock: (block) => {
- return {block.type + " not implemented"} ;
+ const textContent = (block.content as StyledText[])[0]?.text || "";
+ const lines = textContent.split("\n").map((line, index) => {
+ const indent = line.match(/^\s*/)?.[0].length || 0;
+
+ return (
+
+ {line.trimStart() || <> >}
+
+ );
+ });
+
+ return (
+
+ {lines}
+
+ );
+ },
+ pageBreak: () => {
+ return ;
},
audio: (block, exporter) => {
return (
diff --git a/packages/xl-pdf-exporter/src/pdf/defaultSchema/styles.tsx b/packages/xl-pdf-exporter/src/pdf/defaultSchema/styles.tsx
index c2e5ed98b4..40ee152c3f 100644
--- a/packages/xl-pdf-exporter/src/pdf/defaultSchema/styles.tsx
+++ b/packages/xl-pdf-exporter/src/pdf/defaultSchema/styles.tsx
@@ -62,7 +62,7 @@ export const pdfStyleMappingForDefaultSchema: StyleMapping<
return {};
}
return {
- fontFamily: "Courier",
+ fontFamily: "GeistMono",
};
},
};
diff --git a/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx b/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx
index e21fed3ab7..0d7df11349 100644
--- a/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx
+++ b/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx
@@ -6,10 +6,11 @@ import {
defaultBlockSpecs,
defaultInlineContentSpecs,
defaultStyleSpecs,
+ PageBreak,
} from "@blocknote/core";
import { Text } from "@react-pdf/renderer";
import { testDocument } from "@shared/testDocument.js";
-import { prettyDOM, render } from "@testing-library/react";
+import reactElementToJSXString from "react-element-to-jsx-string";
import { describe, expect, it } from "vitest";
import { pdfDefaultSchemaMappings } from "./defaultSchema/index.js";
import { PDFExporter } from "./pdfExporter.js";
@@ -26,6 +27,7 @@ describe("exporter", () => {
const schema = BlockNoteSchema.create({
blockSpecs: {
...defaultBlockSpecs,
+ pageBreak: PageBreak,
extraBlock: createBlockSpec(
{
content: "none",
@@ -155,13 +157,15 @@ describe("exporter", () => {
it("should export a document", async () => {
const exporter = new PDFExporter(
- BlockNoteSchema.create(),
+ BlockNoteSchema.create({
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
+ }),
pdfDefaultSchemaMappings
);
const transformed = await exporter.toReactPDFDocument(testDocument);
- const view = render(transformed);
- const str = prettyDOM(view.container, undefined, { highlight: false });
+ const str = reactElementToJSXString(transformed);
+
expect(str).toMatchFileSnapshot("__snapshots__/example.jsx");
// would be nice to compare pdf images, but currently doesn't work on mac os (due to node canvas installation issue)
@@ -186,7 +190,9 @@ describe("exporter", () => {
it("should export a document with header and footer", async () => {
const exporter = new PDFExporter(
- BlockNoteSchema.create(),
+ BlockNoteSchema.create({
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
+ }),
pdfDefaultSchemaMappings
);
@@ -194,8 +200,7 @@ describe("exporter", () => {
header: Header ,
footer: Footer ,
});
- const view = render(transformed);
- const str = prettyDOM(view.container, undefined, { highlight: false });
+ const str = reactElementToJSXString(transformed);
expect(str).toMatchFileSnapshot(
"__snapshots__/exampleWithHeaderAndFooter.jsx"
);
diff --git a/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx b/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx
index 969f1ce759..aa4b03d4a9 100644
--- a/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx
+++ b/packages/xl-pdf-exporter/src/pdf/pdfExporter.tsx
@@ -63,7 +63,6 @@ export class PDFExporter<
fontSize: FONT_SIZE * PIXELS_PER_POINT, // pixels
lineHeight: 1.5,
},
- section: {},
block: {},
blockChildren: {},
header: {},
@@ -145,6 +144,11 @@ export class PDFExporter<
numberedListIndex
); // TODO: any
+ if (b.type === "pageBreak") {
+ ret.push(self);
+ continue;
+ }
+
const style = this.blocknoteDefaultPropsToReactPDFStyle(b.props as any);
ret.push(
<>
@@ -216,6 +220,14 @@ export class PDFExporter<
fontWeight: "bold",
});
+ font = await loadFontDataUrl(
+ await import("@shared/assets/fonts/GeistMono-Regular.ttf")
+ );
+ Font.register({
+ family: "GeistMono",
+ src: font,
+ });
+
this.fontsRegistered = true;
}
@@ -247,9 +259,7 @@ export class PDFExporter<
{options.header}
)}
-
- {await this.transformBlocks(blocks)}
-
+ {await this.transformBlocks(blocks)}
{options.footer && (
({
@@ -53,7 +57,16 @@ export default defineConfig((conf) => ({
if (deps.includes(source)) {
return true;
}
- return source.startsWith("prosemirror-");
+
+ if (source === "react/jsx-runtime") {
+ return true;
+ }
+
+ if (source.startsWith("prosemirror-")) {
+ return true;
+ }
+
+ return false;
},
output: {
// Provide global variables to use in the UMD build
diff --git a/playground/package.json b/playground/package.json
index ca5a6c881f..02f648e9c3 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -1,7 +1,7 @@
{
"name": "@blocknote/example-editor",
"private": true,
- "version": "0.22.0",
+ "version": "0.23.1",
"scripts": {
"dev": "vite --host",
"build": "tsc && vite build",
@@ -12,22 +12,23 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.609.0",
"@aws-sdk/s3-request-presigner": "^3.609.0",
- "@blocknote/ariakit": "^0.22.0",
- "@blocknote/core": "^0.22.0",
- "@blocknote/mantine": "^0.22.0",
- "@blocknote/react": "^0.22.0",
- "@blocknote/server-util": "^0.22.0",
- "@blocknote/shadcn": "^0.22.0",
- "@blocknote/xl-multi-column": "^0.22.0",
+ "@blocknote/ariakit": "^0.23.1",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/mantine": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
+ "@blocknote/server-util": "^0.23.1",
+ "@blocknote/shadcn": "^0.23.1",
+ "@blocknote/xl-multi-column": "^0.23.1",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@tiptap/suggestion": "^2.7.1",
"@tiptap/core": "^2.7.1",
"@tiptap/react": "^2.7.1",
- "@liveblocks/client": "^2.11.0",
- "@liveblocks/react": "^2.11.0",
- "@liveblocks/react-ui": "^2.11.0",
- "@liveblocks/yjs": "^2.11.0",
+ "@liveblocks/client": "file:../../liveblocks/packages/liveblocks-client",
+ "@liveblocks/react": "file:../../liveblocks/packages/liveblocks-react",
+ "@liveblocks/react-blocknote": "file:../../liveblocks/packages/liveblocks-react-blocknote",
+ "@liveblocks/react-ui": "file:../../liveblocks/packages/liveblocks-react-ui",
+ "@liveblocks/yjs": "file:../../liveblocks/packages/liveblocks-yjs",
"@mantine/core": "^7.10.1",
"@mui/icons-material": "^5.16.1",
"@mui/material": "^5.16.1",
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index 04b3697949..515530851f 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -65,9 +65,9 @@
}
},
{
- "projectSlug": "all-blocks",
- "fullSlug": "basic/all-blocks",
- "pathFromRoot": "examples/01-basic/04-all-blocks",
+ "projectSlug": "default-blocks",
+ "fullSlug": "basic/default-blocks",
+ "pathFromRoot": "examples/01-basic/04-default-blocks",
"config": {
"playground": true,
"docs": true,
@@ -76,10 +76,7 @@
"Basic",
"Blocks",
"Inline Content"
- ],
- "dependencies": {
- "@blocknote/xl-multi-column": "latest"
- } as any
+ ]
},
"title": "Default Schema Showcase",
"group": {
@@ -217,6 +214,24 @@
"slug": "basic"
}
},
+ {
+ "projectSlug": "multi-editor",
+ "fullSlug": "basic/multi-editor",
+ "pathFromRoot": "examples/01-basic/12-multi-editor",
+ "config": {
+ "playground": true,
+ "docs": true,
+ "author": "areknawo",
+ "tags": [
+ "Basic"
+ ]
+ },
+ "title": "Multi-Editor Setup",
+ "group": {
+ "pathFromRoot": "examples/01-basic",
+ "slug": "basic"
+ }
+ },
{
"projectSlug": "testing",
"fullSlug": "basic/testing",
diff --git a/playground/src/style.css b/playground/src/style.css
index 7e716f2b47..43b27bc082 100644
--- a/playground/src/style.css
+++ b/playground/src/style.css
@@ -10,6 +10,10 @@ body {
max-width: 731px;
}
+.bn-comment-composer .bn-container {
+ padding-top: 0;
+}
+
.mantine-AppShell-navbar {
background-color: #f7f7f5;
}
diff --git a/playground/tsconfig.json b/playground/tsconfig.json
index 6ca41017b8..04c99061c5 100644
--- a/playground/tsconfig.json
+++ b/playground/tsconfig.json
@@ -23,6 +23,8 @@
{ "path": "./tsconfig.node.json" },
{ "path": "../packages/core/" },
{ "path": "../packages/react/" },
+ { "path": "../packages/ariakit/" },
+ { "path": "../packages/mantine/" },
{ "path": "../packages/shadcn/" },
{ "path": "../packages/xl-pdf-exporter/" },
{ "path": "../packages/xl-docx-exporter/" },
diff --git a/shared/assets/fonts/GeistMono-Regular.ttf b/shared/assets/fonts/GeistMono-Regular.ttf
new file mode 100644
index 0000000000..8c09dd96a8
Binary files /dev/null and b/shared/assets/fonts/GeistMono-Regular.ttf differ
diff --git a/shared/package.json b/shared/package.json
index 70a1d1f3d6..a508a0573a 100644
--- a/shared/package.json
+++ b/shared/package.json
@@ -2,7 +2,7 @@
"name": "@blocknote/shared",
"homepage": "https://github.com/TypeCellOS/BlockNote",
"private": true,
- "version": "0.22.0",
+ "version": "0.23.1",
"files": [
"dist",
"types",
@@ -13,7 +13,7 @@
"clean": "tsc --build --clean"
},
"dependencies": {
- "@blocknote/core": "^0.22.0"
+ "@blocknote/core": "^0.23.1"
},
"devDependencies": {
"typescript": "^5.3.3"
diff --git a/shared/testDocument.ts b/shared/testDocument.ts
index 281e5f9d7b..ccee204029 100644
--- a/shared/testDocument.ts
+++ b/shared/testDocument.ts
@@ -1,10 +1,14 @@
import {
BlockNoteSchema,
+ defaultBlockSpecs,
+ PageBreak,
partialBlocksToBlocksForTesting,
} from "@blocknote/core";
export const testDocument = partialBlocksToBlocksForTesting(
- BlockNoteSchema.create(),
+ BlockNoteSchema.create({
+ blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak },
+ }),
[
{
type: "paragraph",
@@ -75,10 +79,7 @@ export const testDocument = partialBlocksToBlocksForTesting(
textAlignment: "justify",
},
},
- {
- type: "codeBlock",
- content: "Code Block\nLine 2",
- },
+ { type: "pageBreak" },
{
type: "bulletListItem",
content:
@@ -270,5 +271,14 @@ export const testDocument = partialBlocksToBlocksForTesting(
],
},
},
+ {
+ type: "codeBlock",
+ props: {
+ language: "javascript",
+ },
+ content: `const helloWorld = (message) => {
+ console.log("Hello World", message);
+};`,
+ },
]
);
diff --git a/tests/package.json b/tests/package.json
index b63f7001c3..8960ae885b 100644
--- a/tests/package.json
+++ b/tests/package.json
@@ -1,7 +1,7 @@
{
"name": "@blocknote/tests",
"private": true,
- "version": "0.22.0",
+ "version": "0.23.1",
"scripts": {
"build": "tsc",
"lint": "eslint src --max-warnings 0",
@@ -17,11 +17,11 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
- "@blocknote/ariakit": "^0.22.0",
- "@blocknote/core": "^0.22.0",
- "@blocknote/mantine": "^0.22.0",
- "@blocknote/react": "^0.22.0",
- "@blocknote/shadcn": "^0.22.0",
+ "@blocknote/ariakit": "^0.23.1",
+ "@blocknote/core": "^0.23.1",
+ "@blocknote/mantine": "^0.23.1",
+ "@blocknote/react": "^0.23.1",
+ "@blocknote/shadcn": "^0.23.1",
"@playwright/experimental-ct-react": "^1.49.1",
"@playwright/test": "^1.49.1",
"eslint": "^8.10.0",
diff --git a/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json b/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json
index a777ca1166..efc45cfa6f 100644
--- a/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json
+++ b/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json
@@ -101,7 +101,7 @@
"type": "heading",
"attrs": {
"textAlignment": "left",
- "level": null
+ "level": 1
},
"content": [
{