Skip to content

feat: markdown pasting & custom paste handlers #1490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/pages/docs/advanced/paste-handling.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be added to sidebar

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is in the sidebar?

title: Paste Handling
description: This section explains how to handle paste events in BlockNote.
imageTitle: Paste Handling
---

import { Example } from "@/components/example";

# Paste Handling

BlockNote, by default, attempts to paste content in the following order:

- VS Code compatible content
- BlockNote HTML
- Markdown
- HTML
- Plain text
- Files

By default, in certain cases, BlockNote will attempt to convert the content of the clipboard from plain text to HTML by interpreting the plain text as markdown.

You can change this behavior by providing a custom paste handler.

```ts
const editor = new BlockNoteEditor({
pasteHandler: ({ defaultPasteHandler }) => {
return defaultPasteHandler({
pasteBehavior: "prefer-html",
});
},
});
```

In this example, we change the default paste behavior to prefer HTML.


<Example name="basic/custom-paste-handler" />

## Custom Paste Handler

You can also provide your own paste handler by providing a function to the `pasteHandler` option.

```ts
const editor = new BlockNoteEditor({
pasteHandler: ({ event, editor, defaultPasteHandler }) => {
if (event.clipboardData?.types.includes("text/my-custom-format")) {
const markdown = myCustomTransformer(event.clipboardData.getData("text/my-custom-format"));
editor.pasteText(markdown);
// We handled the paste event, so return true
return true;
}

// If we didn't handle the paste event, call the default paste handler to do the default behavior
return defaultPasteHandler();
},
});
```

In this example, we handle the paste event if the clipboard data contains `text/my-custom-format`. If we don't handle the paste event, we call the default paste handler to do the default behavior.
9 changes: 9 additions & 0 deletions docs/pages/docs/editor-basics/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ type BlockNoteEditorOptions = {
class?: string;
}) => Plugin;
initialContent?: PartialBlock[];
pasteHandler?: (context: {
event: ClipboardEvent;
editor: BlockNoteEditor;
defaultPasteHandler: (context: {
pasteBehavior?: "prefer-markdown" | "prefer-html";
}) => boolean | undefined;
}) => boolean | undefined;
resolveFileUrl: (url: string) => Promise<string>
schema?: BlockNoteSchema;
setIdAttribute?: boolean;
Expand Down Expand Up @@ -66,6 +73,8 @@ The hook takes two optional parameters:

`initialContent:` The content that should be in the editor when it's created, represented as an array of [Partial Blocks](/docs/manipulating-blocks#partial-blocks).

`pasteHandler`: A function that can be used to override the default paste behavior. See [Paste Handling](/docs/advanced/paste-handling) for more.

`resolveFileUrl:` Function to resolve file URLs for display/download. Useful for creating authenticated URLs or implementing custom protocols.

`resolveUsers`: Function to resolve user information for comments. See [Comments](/docs/collaboration/comments) for more.
Expand Down
6 changes: 6 additions & 0 deletions examples/01-basic/13-custom-paste-handler/.bnexample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"playground": true,
"docs": true,
"author": "nperez0111",
"tags": ["Basic"]
}
105 changes: 105 additions & 0 deletions examples/01-basic/13-custom-paste-handler/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useCreateBlockNote } from "@blocknote/react";

import "./styles.css";

export default function App() {
// Creates a new editor instance.
const editor = useCreateBlockNote({
initialContent: [
{
type: "paragraph",
content: [
{
styles: {},
type: "text",
text: "Paste some text here",
},
],
},
],
pasteHandler: ({ event, editor, defaultPasteHandler }) => {
if (event.clipboardData?.types.includes("text/plain")) {
editor.pasteMarkdown(
event.clipboardData.getData("text/plain") +
" - inserted by the custom paste handler"
);
return true;
}
return defaultPasteHandler();
},
});

// Renders the editor instance using a React component.
return (
<div>
<BlockNoteView editor={editor} />
<div className={"edit-buttons"}>
<button
className={"edit-button"}
onClick={async () => {
try {
await navigator.clipboard.writeText(
"**This is markdown in the plain text format**"
);
} catch (error) {
window.alert("Failed to copy plain text with markdown content");
}
}}>
Copy text/plain with markdown content
</button>
<button
className={"edit-button"}
onClick={async () => {
try {
await navigator.clipboard.write([
new ClipboardItem({
"text/html": "<p><strong>HTML</strong></p>",
}),
]);
} catch (error) {
window.alert("Failed to copy HTML content");
}
}}>
Copy text/html with HTML content
</button>
<button
className={"edit-button"}
onClick={async () => {
try {
await navigator.clipboard.writeText(
"This is plain text in the plain text format"
);
} catch (error) {
window.alert("Failed to copy plain text");
}
}}>
Copy plain text
</button>
<button
className={"edit-button"}
onClick={async () => {
try {
await navigator.clipboard.write([
new ClipboardItem({
"text/plain": "Plain text",
}),
new ClipboardItem({
"text/html": "<p><strong>HTML</strong></p>",
}),
new ClipboardItem({
"text/markdown": "**Markdown**",
}),
]);
} catch (error) {
window.alert("Failed to copy multiple formats");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting this error when I click this button :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not supported by most browsers, but works in Safari. I marked as such

}
}}>
Copy multiple formats
</button>
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions examples/01-basic/13-custom-paste-handler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Custom Paste Handler

In this example, we show how to change the default paste handler to handle paste events in your own way.

**Relevant Docs:**

- [Paste Handling](/docs/advanced/paste-handling)
14 changes: 14 additions & 0 deletions examples/01-basic/13-custom-paste-handler/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html lang="en">
<head>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Paste Handler</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/01-basic/13-custom-paste-handler/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const root = createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
37 changes: 37 additions & 0 deletions examples/01-basic/13-custom-paste-handler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@blocknote/example-custom-paste-handler",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"private": true,
"version": "0.12.4",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
"@blocknote/core": "latest",
"@blocknote/react": "latest",
"@blocknote/ariakit": "latest",
"@blocknote/mantine": "latest",
"@blocknote/shadcn": "latest",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.10.0",
"vite": "^5.3.4"
},
"eslintConfig": {
"extends": [
"../../../.eslintrc.js"
]
},
"eslintIgnore": [
"dist"
]
}
15 changes: 15 additions & 0 deletions examples/01-basic/13-custom-paste-handler/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.edit-buttons {
display: flex;
justify-content: space-between;
margin-top: 8px;
}

.edit-button {
border: 1px solid gray;
border-radius: 4px;
padding-inline: 4px;
}

.edit-button:hover {
border: 1px solid lightgrey;
}
36 changes: 36 additions & 0 deletions examples/01-basic/13-custom-paste-handler/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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/"
}
]
}
32 changes: 32 additions & 0 deletions examples/01-basic/13-custom-paste-handler/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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),
},
}));
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const acceptedMIMETypes = [
"vscode-editor-data",
"blocknote/html",
"text/markdown",
"text/html",
"text/plain",
"Files",
Expand Down
Loading
Loading