diff --git a/examples/01-basic/17-no-trailing-block/.bnexample.json b/examples/01-basic/17-no-trailing-block/.bnexample.json new file mode 100644 index 0000000000..e9c8bcb27b --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Basic"] +} diff --git a/examples/01-basic/17-no-trailing-block/README.md b/examples/01-basic/17-no-trailing-block/README.md new file mode 100644 index 0000000000..9c63e46fdd --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/README.md @@ -0,0 +1,7 @@ +# No Trailing Block + +This example shows how to disable the automatic creation of a trailing block at the end of the editor by setting the `trailingBlock` option to `false`. + +**Relevant Docs:** + +- [Editor Setup](/docs/getting-started/editor-setup) diff --git a/examples/01-basic/17-no-trailing-block/index.html b/examples/01-basic/17-no-trailing-block/index.html new file mode 100644 index 0000000000..a86933f050 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/index.html @@ -0,0 +1,14 @@ + + + + + No Trailing Block + + + +
+ + + diff --git a/examples/01-basic/17-no-trailing-block/main.tsx b/examples/01-basic/17-no-trailing-block/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/01-basic/17-no-trailing-block/package.json b/examples/01-basic/17-no-trailing-block/package.json new file mode 100644 index 0000000000..84ad1d6734 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/package.json @@ -0,0 +1,31 @@ +{ + "name": "@blocknote/example-basic-no-trailing-block", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^8.3.11", + "@mantine/hooks": "^8.3.11", + "@mantine/utils": "^6.0.22", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" + } +} \ No newline at end of file diff --git a/examples/01-basic/17-no-trailing-block/src/App.tsx b/examples/01-basic/17-no-trailing-block/src/App.tsx new file mode 100644 index 0000000000..ac51ec74b3 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/src/App.tsx @@ -0,0 +1,14 @@ +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + trailingBlock: false, + }); + + // Renders the editor instance using a React component. + return ; +} diff --git a/examples/01-basic/17-no-trailing-block/tsconfig.json b/examples/01-basic/17-no-trailing-block/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/01-basic/17-no-trailing-block/vite.config.ts b/examples/01-basic/17-no-trailing-block/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index bd4a517900..015cf22420 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -9,6 +9,7 @@ import { } from "../../schema/index.js"; import { formatKeyboardShortcut } from "../../util/browser.js"; import { FilePanelExtension } from "../FilePanel/FilePanel.js"; +import { FormattingToolbarExtension } from "../FormattingToolbar/FormattingToolbar.js"; import { DefaultSuggestionItem } from "./DefaultSuggestionItem.js"; import { SuggestionMenu } from "./SuggestionMenu.js"; @@ -242,6 +243,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "image", ...editor.dictionary.slash_menu.image, @@ -257,6 +263,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "video", ...editor.dictionary.slash_menu.video, @@ -272,6 +283,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "audio", ...editor.dictionary.slash_menu.audio, @@ -287,6 +303,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "file", ...editor.dictionary.slash_menu.file, diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index ae117ce392..868ff5b7aa 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -323,6 +323,25 @@ }, "readme": "This example makes the editor read-only while showing the same content as the [Default Schema Showcase](/examples/basic/default-blocks) example.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Document Structure](/docs/foundations/document-structure)\n- [Default Schema](/docs/foundations/schemas)" }, + { + "projectSlug": "no-trailing-block", + "fullSlug": "basic/no-trailing-block", + "pathFromRoot": "examples/01-basic/17-no-trailing-block", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "No Trailing Block", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + }, + "readme": "This example shows how to disable the automatic creation of a trailing block at the end of the editor by setting the `trailingBlock` option to `false`.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, { "projectSlug": "testing", "fullSlug": "basic/testing", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 966cf9e887..8ec1de17a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1110,6 +1110,52 @@ importers: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3) + examples/01-basic/17-no-trailing-block: + dependencies: + '@blocknote/ariakit': + specifier: latest + version: link:../../../packages/ariakit + '@blocknote/core': + specifier: latest + version: link:../../../packages/core + '@blocknote/mantine': + specifier: latest + version: link:../../../packages/mantine + '@blocknote/react': + specifier: latest + version: link:../../../packages/react + '@blocknote/shadcn': + specifier: latest + version: link:../../../packages/shadcn + '@mantine/core': + specifier: ^8.3.11 + version: 8.3.18(@mantine/hooks@8.3.18(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@mantine/hooks': + specifier: ^8.3.11 + version: 8.3.18(react@19.2.5) + '@mantine/utils': + specifier: ^6.0.22 + version: 6.0.22(react@19.2.5) + react: + specifier: ^19.2.3 + version: 19.2.5 + react-dom: + specifier: ^19.2.3 + version: 19.2.5(react@19.2.5) + devDependencies: + '@types/react': + specifier: ^19.2.3 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3)) + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3) + examples/01-basic/testing: dependencies: '@blocknote/ariakit': @@ -24078,8 +24124,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.2 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) @@ -24128,7 +24174,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -24139,7 +24185,7 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -24153,14 +24199,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -24201,7 +24247,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -24212,7 +24258,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/tests/src/end-to-end/images/images.test.ts b/tests/src/end-to-end/images/images.test.ts index b4c7dd2910..83f9045152 100644 --- a/tests/src/end-to-end/images/images.test.ts +++ b/tests/src/end-to-end/images/images.test.ts @@ -1,18 +1,15 @@ import { FileChooser, expect } from "@playwright/test"; import { test } from "../../setup/setupScript.js"; -import { BASE_URL } from "../../utils/const.js"; +import { BASE_URL, NO_TRAILING_BLOCK_URL } from "../../utils/const.js"; import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor.js"; import { executeSlashCommand } from "../../utils/slashmenu.js"; const IMAGE_UPLOAD_PATH = "src/end-to-end/images/placeholder.png"; const IMAGE_EMBED_URL = "https://placehold.co/800x540.png"; -test.beforeEach(async ({ page }) => { - await page.goto(BASE_URL); -}); - test.describe("Check Image Block and Toolbar functionality", () => { test("Should be able to create image block", async ({ page }) => { + await page.goto(BASE_URL); await focusOnEditor(page); await executeSlashCommand(page, "image"); @@ -21,6 +18,7 @@ test.describe("Check Image Block and Toolbar functionality", () => { expect(await page.screenshot()).toMatchSnapshot("create-image.png"); }); test.skip("Should be able to upload image", async ({ page }) => { + await page.goto(BASE_URL); await focusOnEditor(page); await executeSlashCommand(page, "image"); @@ -37,6 +35,7 @@ test.describe("Check Image Block and Toolbar functionality", () => { expect(await page.screenshot()).toMatchSnapshot("upload-image.png"); }); test("Should be able to embed image", async ({ page }) => { + await page.goto(BASE_URL); await focusOnEditor(page); await executeSlashCommand(page, "image"); @@ -54,6 +53,7 @@ test.describe("Check Image Block and Toolbar functionality", () => { expect(await page.screenshot()).toMatchSnapshot("embed-image.png"); }); test("Should be able to resize image", async ({ page }) => { + await page.goto(BASE_URL); await focusOnEditor(page); await executeSlashCommand(page, "image"); @@ -95,6 +95,7 @@ test.describe("Check Image Block and Toolbar functionality", () => { expect(await page.screenshot()).toMatchSnapshot("resize-image.png"); }); test("Should be able to delete image with backspace", async ({ page }) => { + await page.goto(BASE_URL); await focusOnEditor(page); await executeSlashCommand(page, "image"); @@ -109,4 +110,17 @@ test.describe("Check Image Block and Toolbar functionality", () => { await compareDocToSnapshot(page, "deleteImage"); }); + test("Should open file panel but not formatting toolbar when inserting image with no trailing block", async ({ + page, + }) => { + await page.goto(NO_TRAILING_BLOCK_URL); + await focusOnEditor(page); + await executeSlashCommand(page, "image"); + + const filePanel = page.locator(".bn-panel"); + await expect(filePanel).toBeVisible(); + + const formattingToolbar = page.locator(".bn-formatting-toolbar"); + await expect(formattingToolbar).not.toBeVisible(); + }); }); diff --git a/tests/src/utils/const.ts b/tests/src/utils/const.ts index 7431d2db2a..f7ef0eac35 100644 --- a/tests/src/utils/const.ts +++ b/tests/src/utils/const.ts @@ -51,6 +51,10 @@ export const COMMENTS_URL = !process.env.RUN_IN_DOCKER ? `http://localhost:${PORT}/collaboration/comments-testing?hideMenu` : `http://host.docker.internal:${PORT}/collaboration/comments-testing?hideMenu`; +export const NO_TRAILING_BLOCK_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/basic/no-trailing-block?hideMenu` + : `http://host.docker.internal:${PORT}/basic/no-trailing-block?hideMenu`; + export const PASTE_ZONE_SELECTOR = "#pasteZone"; export const EDITOR_SELECTOR = `.bn-editor`;