diff --git a/examples/06-custom-schema/08-non-editable-block/.bnexample.json b/examples/06-custom-schema/08-non-editable-block/.bnexample.json new file mode 100644 index 0000000000..c94d1e9154 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Intermediate", "Blocks", "Custom Schemas"] +} diff --git a/examples/06-custom-schema/08-non-editable-block/README.md b/examples/06-custom-schema/08-non-editable-block/README.md new file mode 100644 index 0000000000..9c7cce19d1 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/README.md @@ -0,0 +1,8 @@ +# Non-Editable Block + +In this example, we create a custom block which renders a simple HTML paragraph with placeholder text. The block has no editable content. + +**Relevant Docs:** + +- [Custom Blocks](/docs/features/custom-schemas/custom-blocks) +- [Editor Setup](/docs/getting-started/editor-setup) diff --git a/examples/06-custom-schema/08-non-editable-block/index.html b/examples/06-custom-schema/08-non-editable-block/index.html new file mode 100644 index 0000000000..9b55422066 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/index.html @@ -0,0 +1,14 @@ + + + + + Non-Editable Block + + + +
+ + + diff --git a/examples/06-custom-schema/08-non-editable-block/main.tsx b/examples/06-custom-schema/08-non-editable-block/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-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/06-custom-schema/08-non-editable-block/package.json b/examples/06-custom-schema/08-non-editable-block/package.json new file mode 100644 index 0000000000..525adc0964 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/package.json @@ -0,0 +1,31 @@ +{ + "name": "@blocknote/example-custom-schema-non-editable-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/06-custom-schema/08-non-editable-block/src/App.tsx b/examples/06-custom-schema/08-non-editable-block/src/App.tsx new file mode 100644 index 0000000000..78a611081f --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/src/App.tsx @@ -0,0 +1,38 @@ +import { BlockNoteSchema } 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 { createNonEditableBlock } from "./NonEditableBlock"; + +// Our schema with block specs, which contain the configs and implementations for +// blocks that we want our editor to use. +const schema = BlockNoteSchema.create().extend({ + blockSpecs: { + // Creates an instance of the Non-Editable block and adds it to the schema. + nonEditable: createNonEditableBlock(), + }, +}); + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + schema, + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "nonEditable", + }, + { + type: "paragraph", + }, + ], + }); + + // Renders the editor instance. + return ; +} diff --git a/examples/06-custom-schema/08-non-editable-block/src/NonEditableBlock.tsx b/examples/06-custom-schema/08-non-editable-block/src/NonEditableBlock.tsx new file mode 100644 index 0000000000..a930c21f74 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/src/NonEditableBlock.tsx @@ -0,0 +1,13 @@ +import { createReactBlockSpec } from "@blocknote/react"; + +// The Non-Editable block. +export const createNonEditableBlock = createReactBlockSpec( + { + type: "nonEditable", + propSchema: {}, + content: "none", + }, + { + render: () =>

This is a non-editable block.

, + }, +); diff --git a/examples/06-custom-schema/08-non-editable-block/tsconfig.json b/examples/06-custom-schema/08-non-editable-block/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-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/06-custom-schema/08-non-editable-block/vite.config.ts b/examples/06-custom-schema/08-non-editable-block/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-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/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 5f219ff7fa..ae117ce392 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1414,6 +1414,27 @@ }, "readme": "This example shows how you can configure the editor's default blocks. Specifically, heading blocks are made to only support levels 1-3, and cannot be toggleable.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Default Schema](/docs/foundations/schemas)\n- [Custom Schemas](/docs/features/custom-schemas)" }, + { + "projectSlug": "non-editable-block", + "fullSlug": "custom-schema/non-editable-block", + "pathFromRoot": "examples/06-custom-schema/08-non-editable-block", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "Custom Schemas" + ] + }, + "title": "Non-Editable Block", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + }, + "readme": "In this example, we create a custom block which renders a simple HTML paragraph with placeholder text. The block has no editable content.\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, { "projectSlug": "draggable-inline-content", "fullSlug": "custom-schema/draggable-inline-content", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 317c09c8b5..46cc6ca3b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3395,6 +3395,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/06-custom-schema/08-non-editable-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/06-custom-schema/draggable-inline-content: dependencies: '@blocknote/ariakit': @@ -24027,8 +24073,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)) @@ -24077,7 +24123,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 @@ -24088,7 +24134,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 @@ -24102,14 +24148,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 @@ -24150,7 +24196,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 @@ -24161,7 +24207,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/copypaste/copypaste.test.ts b/tests/src/end-to-end/copypaste/copypaste.test.ts index aaf36897d6..b0a563c6ef 100644 --- a/tests/src/end-to-end/copypaste/copypaste.test.ts +++ b/tests/src/end-to-end/copypaste/copypaste.test.ts @@ -1,6 +1,10 @@ /* eslint-disable jest/valid-title */ import { test } from "../../setup/setupScript.js"; -import { BASE_URL } from "../../utils/const.js"; +import { + BASE_URL, + NON_EDITABLE_BLOCK_URL, + PARAGRAPH_SELECTOR, +} from "../../utils/const.js"; import { copyPaste, copyPasteAll, @@ -15,11 +19,11 @@ import { executeSlashCommand } from "../../utils/slashmenu.js"; test.describe.configure({ mode: "serial" }); -test.beforeEach(async ({ page }) => { - await page.goto(BASE_URL); -}); - test.describe("Check Copy/Paste Functionality", () => { + test.beforeEach(async ({ page }) => { + await page.goto(BASE_URL); + }); + test("Paragraphs should stay separate", async ({ page, browserName }) => { test.skip( browserName === "firefox" || browserName === "webkit", @@ -188,3 +192,40 @@ test.describe("Check Copy/Paste Functionality", () => { await compareDocToSnapshot(page, "images.json"); }); }); + +test.describe("Check Copy/Paste From Non-Editable Block", () => { + test.beforeEach(async ({ page }) => { + await page.goto(NON_EDITABLE_BLOCK_URL); + }); + + test("Should be able to copy/paste text from a non-editable block", async ({ + page, + browserName, + }) => { + test.skip( + browserName === "firefox" || browserName === "webkit", + "Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33.", + ); + + // Click and drag across the non-editable block's text to select part of it. + const p = page.locator('[data-content-type="nonEditable"] p'); + const box = (await p.boundingBox())!; + await page.mouse.move(box.x + 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move( + box.x + box.width * 0.25, + box.y + box.height / 2, + { steps: 5 }, + ); + await page.mouse.up(); + + await page.keyboard.press("ControlOrMeta+C"); + + // Click the last (empty) paragraph block to focus the editor. + await page.locator(PARAGRAPH_SELECTOR).last().click(); + + await page.keyboard.press("ControlOrMeta+V"); + + await compareDocToSnapshot(page, "nonEditableBlock.json"); + }); +}); diff --git a/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/nonEditableBlock-json-chromium-linux.json b/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/nonEditableBlock-json-chromium-linux.json new file mode 100644 index 0000000000..154f585790 --- /dev/null +++ b/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/nonEditableBlock-json-chromium-linux.json @@ -0,0 +1,95 @@ +{ + "type": "doc", + "content": [ + { + "type": "blockGroup", + "content": [ + { + "type": "blockContainer", + "attrs": { + "id": "0" + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "backgroundColor": "default", + "textColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Welcome to this demo!" + } + ] + } + ] + }, + { + "type": "blockContainer", + "attrs": { + "id": "1" + }, + "content": [ + { + "type": "nonEditable" + } + ] + }, + { + "type": "blockContainer", + "attrs": { + "id": "2" + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "backgroundColor": "default", + "textColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "textColor", + "attrs": { + "stringValue": "rgb(63, 63, 63)" + } + }, + { + "type": "backgroundColor", + "attrs": { + "stringValue": "rgb(255, 255, 255)" + } + } + ], + "text": "This isĀ " + } + ] + } + ] + }, + { + "type": "blockContainer", + "attrs": { + "id": "4" + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "backgroundColor": "default", + "textColor": "default", + "textAlignment": "left" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/src/utils/const.ts b/tests/src/utils/const.ts index b04b77d6a2..7431d2db2a 100644 --- a/tests/src/utils/const.ts +++ b/tests/src/utils/const.ts @@ -43,6 +43,10 @@ export const ALERT_BLOCK_URL = !process.env.RUN_IN_DOCKER ? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu` : `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`; +export const NON_EDITABLE_BLOCK_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/custom-schema/non-editable-block?hideMenu` + : `http://host.docker.internal:${PORT}/custom-schema/non-editable-block?hideMenu`; + 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`;